import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import * as gc from 'src/app/global/const';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { ArrayutilsService } from './arrayutils.service';
import { StringUtilsService } from './string-utils.service';
import { JsonResult } from '../model/JsonResult';
import { MySQLColumnInfo, ServerField } from '../global/model';

@Injectable({
  providedIn: 'root'
})
export class MysqlService {

  //#region MYSQL数据类型映射到Angular的数据类型
  dataTypeMap = {
    "varchar":"string",
    "int":"number",
    "float":"number",
    "datetime":"Date",
    "timestamp":"Date",
    "tinyint":"boolean",
    "decimal":"number",
    "bit":"boolean",
    "BIT":"boolean",
  }
  //#endregion

  //#region MYSQL数据类型映射到 Mybatis 的数据类型
  dbDataType2MybatisDataType = {
    'int':'INTEGER',
    'varchar':'VARCHAR',
    'datetime':'TIMESTAMP',
    'tinyint':'BIT',
    'decimal':'DECIMAL',
    'float':'REAL'
  }
  //#endregion

  //#region MYSQL数据类型对应 JAVA 的数据类型
  dbDataType2JavaDataType = {
    'varchar':'String',
    'int':'Integer',
    'datetime':'Date',
    'decimal':'BigDecimal',
    'float':'Float',
  }
  //#endregion

  //#region 数据库对象类型
  objType = {'database':'database','table':'table','field':'field'}
  //#endregion

  //#region 常用SQL语句
  sql = {
    'SHOW DATABASES':'select * from information_schema.`SCHEMATA` ORDER BY schema_name ASC;',

    'SHOW TABLES':`SELECT table_schema,table_name,create_time,update_time,
    table_collation,table_comment,engine
    FROM information_schema.tables
    WHERE table_schema='weixin_cloud' AND table_type='base table' ORDER BY table_name;`,

    'GET_DATABASE_TABLES':`select a.SCHEMA_NAME AS schema_name,b.table_name,b.table_comment
    from information_schema.\`SCHEMATA\` a
    LEFT JOIN information_schema.tables b
    ON a.SCHEMA_NAME = b.table_schema
    WHERE 1=1;`,

    // 返回的字段有：auto_key,schema_name,table_name,table_comment,column_name,column_type,
    // data_type,column_comment,extra,column_key
    'GET_TABLE_FIELDS':`select * from information_schema.\`COLUMNS\` WHERE table_schema='DBNAME' 
      AND table_name='TABLENAME';`,

      // c.extra = auto_increment 表示该字段是自增字段
      // c.column_key = PRI 表示该字段是主键
    'GET_DB_TABLES_FIELDS':`SELECT @auto_key:= @auto_key + 1 AS auto_key,d.*
    FROM (
    select a.SCHEMA_NAME AS schema_name,b.table_name,b.table_comment,
    c.column_name,c.column_type,c.data_type,c.column_comment,c.extra,
    c.column_key
    from information_schema.\`SCHEMATA\` a
    LEFT JOIN information_schema.tables b
    ON a.SCHEMA_NAME = b.table_schema
    LEFT JOIN information_schema.\`COLUMNS\` c
    ON a.SCHEMA_NAME = c.table_schema
    AND b.table_name = c.table_name
    WHERE 1=1
    ORDER BY a.SCHEMA_NAME,b.table_name,c.ordinal_position
    ) d,(SELECT @auto_key:=0) e;`
  }
  //#endregion

  //#region 请求URL
  url = {
    'queryForList':gc.BACKEND_SERVER + '/dynamicDBController/queryForList'
  }
  //#endregion

  //#region 属性
  //rs:any[];// 请求后端获取到的用于构建树的原始记录集
  rs:ServerField[];// MYSQL服务器所有数据库所有表所有字段
  //#endregion

  constructor(
    private http:HttpClient,
    private arrayService:ArrayutilsService,
    private stringService:StringUtilsService
  ) { }

  //#region 获取服务器的所有数据库
  // 获取服务器中所有数据库名称列表
  // 返回的单个元素的格式是：
  //{CATALOG_NAME: "def", SCHEMA_NAME: "atools", DEFAULT_CHARACTER_SET_NAME: "utf8mb4", DEFAULT_COLLATION_NAME: "utf8mb4_croatian_ci", SQL_PATH: null}
  getDatabases(dbConfig:DBConfig):Observable<any[]>{
    let queryParams = new DynamicQueryParams();
    queryParams.sql = this.sql["SHOW DATABASES"];
    queryParams.dbConfig = dbConfig;

    return this.http.post<any[]>(this.url.queryForList,queryParams);
  }
  //#endregion

  //#region 将数组变形为树形结构
  convertDB2Nodes(dbs$:Observable<any[]>){
    let nodes = dbs$.pipe(map(
      (res:any[])=>{
        let ns:any[] = []
        res.forEach(ele => {
          ns.push({ 'title':ele.SCHEMA_NAME,'key':ele.SCHEMA_NAME });
        });
        return ns;
      }
    ));

    return nodes;
  }
  //#endregion

  //#region 将表数组变形为树形结构
  // 将数据表流的数据转换为 nodes，返回流
  convertTables2Nodes(tables$:Observable<any[]>):Observable<any[]>{
    let nodes = tables$.pipe(map(
      (res:any[])=>{
        let ns = [];
        res.forEach(ele => {
          ns.push({ 'title':ele.table_name + ',' + ele.table_comment,'key':ele.table_name });
        });
        return ns;
      }
    ))

    return nodes;
  }
  //#endregion

  //#region 获取指定数据库下的所有表
  getTables(dbConfig:DBConfig,dbName:string):Observable<any[]>{
    let sql = this.sql["SHOW TABLES"].replace('weixin_cloud',dbName);

    let queryParams = new DynamicQueryParams();
    queryParams.sql = sql;
    queryParams.dbConfig = dbConfig;

    return this.http.post<any[]>(this.url.queryForList,queryParams);
  }
  //#endregion

  //#region 获取数据库下的所有表
  private getDatabasesTables(dbConfig:DBConfig):Observable<any[]>{
    let queryParams = new DynamicQueryParams();
    queryParams.sql = this.sql.GET_DATABASE_TABLES;
    queryParams.dbConfig = dbConfig;

    return this.http.post<any[]>(this.url.queryForList,queryParams);
  }
  //#endregion

  //#region 获取所有字段
  private getAllFields(dbConfig:DBConfig):Observable<any[]>{
    let queryParams = new DynamicQueryParams();
    queryParams.sql = this.sql.GET_DB_TABLES_FIELDS;
    queryParams.dbConfig = dbConfig;

    return this.http.post<any[]>(this.url.queryForList,queryParams);
  }
  //#endregion

  //#region 构建数据库、表、字段构成的树
  buildTableTree(dbConfig:DBConfig){
    return this.getDatabasesTables(dbConfig).pipe(map(
      ( res:any[] )=>{
        return this.convertFlat2Tree(res);
      }
    ));
  }
  //#endregion

  //#region 构建：数据库、表、字段   构成的树
  buildFieldTree(dbConfig:DBConfig){
    let ret$ = this.getAllFields(dbConfig).pipe(map(
      (res:ServerField[])=>{
        this.rs = res;
        return this.convertFlat2FieldTreeWithKey(res);
        // return this.arrayService.buildTree(res,['schema_name','table_name','column_name'],'auto_key');
      }
    ))

    return ret$;
  }
  //#endregion

  //#region 将数据库、表、字段构成的数组变形为树形结构
  // 传递进来的数组的格式是：
  // INSERT INTO MY_TABLE(schema_name, table_name, table_comment, column_name, column_type, column_comment)
  // VALUES ('weixin_log', 'subscribe', '关注公众号的日志', 'openid', 'varchar(300)', '用户openid');
  private convertFlat2FieldTree(rs:any[]):any[]{
    let distDB = [];
    let distTable = [];

    rs.forEach(ele => {
      // 构建去重的数据库数组
      if(distDB.length<=0) {
        distDB.push(ele.schema_name);
      }
      if(!this.arrayService.has(distDB,ele.schema_name)){
        distDB.push(ele.schema_name);
      }

      // 构建去重的表数组
      if(distTable.length<=0) distTable.push({'dbName':ele.schema_name,'tableName':ele.table_name});
      let tableExists = distTable.some((ele1,index,array)=>{
        return ele1.tableName === ele.table_name;
      })
      if( !tableExists ) distTable.push({'dbName':ele.schema_name,'tableName':ele.table_name});

    });

    if(distDB.length<=0){
      console.log('没有数据库！');
      return;
    }

    let tree = [];
    distDB.forEach(ele => {
      let dbNode = {'title':ele,'key':ele,'children':[],'origin':'0'};// origin = 0 表示是数据库

      let tableNodes = [];
      let tables = distTable.filter((item)=>{
        return item.dbName === ele;
      })

      tables.forEach(ele1 => {
        let fields = rs.filter((item)=>{
          return item.table_name === ele1.tableName;
        })

        let fieldNodes = [];
        fields.forEach((ele2) => {
          let title = ele2.column_name + ' - ' + ele2.column_type + ' - ' + ele2.column_comment;
          fieldNodes.push({'title':title,'key':ele2.column_name,'isLeaf':'true','origin':'2'});// origin =2 表示是字段
        });
        let tableNode = {'title':ele1.tableName,'key':ele1.tableName,'children':fieldNodes,'origin':'1'};// origin =1 表示是数据表
        tableNodes.push(tableNode);
      });

      dbNode.children = tableNodes;
      tree.push(dbNode);
    });


    return tree;
  }
  //#endregion


  private buildOrigin(type:string,value:string){
    return {'type':type,'value':value};
  }

  //#region 将异步获取来的数据库以及表转换为树形
  // ========= 使用 level 创建树形数据 Begin ===========================
  buildTreeByLevel(rs:any[],levels:string[]){
    let i = 0;
    for(i=0;i<levels.length;i++){
      let fieldName = levels[i];
      let curDistArray = [];
      if(curDistArray.length<=0) curDistArray.push();
    }
  }
  //#endregion


  buildCurNode(row:any,levels:string[],curLevel:number):string{
    let i = 0;
    let ret='{';
    console.log(row['schema_name']);
    console.log(row['table_name']);

    for(i=0;i<=curLevel;i++){
      let fieldName = levels[i];
      let value = row[fieldName];
      ret = ret + `'value${i}':'${value}',`;
    }
    ret = ret + `'children':[]`;
    // 去掉最后的逗号
    if( ret.substr(ret.length-1,1)===',' ) ret = ret.substr(0,ret.length-1)
    ret = ret + '}';
    return ret;
  }
  // ========= 使用 level 创建树形数据 End ===========================

  //#region 将字段数组构建为树形结构
  private convertFlat2FieldTreeWithKey(rs:any[]):any[]{
    let rsDB = [];
    let rsTable = [];

    // 构建去重的数据库数组、表数组
    rs.forEach(ele => {
      // 构建去重的数据库数组
      if(rsDB.length<=0) rsDB.push({'dbName':ele.schema_name,'autoKey':ele.auto_key});
      let dbExists = rsDB.some(
        (dbRow,index,array) =>{
          return dbRow.dbName === ele.schema_name;
        }
      )
      if( !dbExists ) rsDB.push({'dbName':ele.schema_name,'autoKey':ele.auto_key});

      // 构建去重的表数组
      if(rsTable.length<=0) rsTable.push({'dbName':ele.schema_name,'tableName':ele.table_name,'autoKey':ele.auto_key});
      let tableExists = rsTable.some((ele1,index,array)=>{
        return ele1.tableName === ele.table_name && ele1.dbName === ele.schema_name ;
      })
      if( !tableExists ) rsTable.push({'dbName':ele.schema_name,'tableName':ele.table_name,'autoKey':ele.auto_key});

    });

    // 没有数据库则不创建，退出
    if(rsDB.length<=0){
      console.log('没有数据库！');
      return;
    }

    let tree = [];

    rsDB.forEach(eleDB => {// 遍历数据库数组
      // 构建单个数据库节点 - 子节点留空
      // let dbKey = "schema_name=" + eleDB;
      let dbNode = {'title':eleDB.dbName,'key':eleDB.autoKey,'children':[],'origin':this.buildOrigin(this.objType.database,eleDB.dbName)};// 本地枚举设置类型 = 数据库

      let tables = rsTable.filter((item)=>{// 获取当前数据库下的所有表
        return item.dbName === eleDB.dbName;
      })

      let tableNodes = [];
      tables.forEach(eleTable => {
        let rsFields = rs.filter((item)=>{// 从原始记录集中获取指定数据库、表下的所有字段
          return item.table_name === eleTable.tableName && item.schema_name === eleTable.dbName;
        })

        let fieldNodes = [];
        rsFields.forEach((eleField) => {
          let title = eleField.column_name + ' - ' + eleField.column_type + ' - ' + eleField.column_comment;
          fieldNodes.push({'title':title,'key':eleField.auto_key,'isLeaf':'true','origin':this.buildOrigin(this.objType.field,eleField.column_name)});
        });
        let tableNode = {'title':eleTable.tableName,'key':eleTable.autoKey,'children':fieldNodes,'origin':this.buildOrigin(this.objType.table,eleTable.tableName)};
        tableNodes.push(tableNode);
      });

      dbNode.children = tableNodes;
      tree.push(dbNode);
    });


    return tree;
  }
  //#endregion

  //#region 又是一个变形为树形结构
  convertFlat2Tree(array:any[]):any[]{
    let distArray = [];
    let count = 0;
    array.forEach(ele => {// 在字段 schema_name 上去重后构建一个新的数组
      if( distArray.length<=0 ) distArray.push(ele.schema_name);

      if(!this.arrayService.has(distArray,ele.schema_name)){
        distArray.push(ele.schema_name);
      }
    });

    if( distArray.length<=0 ){
      console.log('没有过滤出数据库列表！');
      return null;
    }

    let tree = [];

    distArray.forEach(ele => {// 遍历去重的数组分别构建其下的子节点
      let node = { 'title':ele,'key':ele,'children':[] };

      let children = [];
      array.filter(( item )=>{// 遍历所有数据构建指定数据库下的表名数组
        if( item.schema_name === ele ){
          children.push({'title':item.table_name,'key':item.table_name,'isLeaf':'false'});
        }
      })

      node.children = children;
      tree.push(node);
    });

    return tree;
  }
  //#endregion

  //#region 获取表字段，返回的数据类型是 MySQLColumnInfo
  getTableFields(dbConfig:DBConfig,dbName:string,tableName:string):Observable<any[]>{
    let sql = this.sql.GET_TABLE_FIELDS.replace('DBNAME',dbName).replace('TABLENAME',tableName);
    let queryParams = new DynamicQueryParams();
    queryParams.sql = sql;
    queryParams.dbConfig = dbConfig;

    return this.http.post<any[]>(this.url.queryForList,queryParams);
  }
  //#endregion

  //#region 判断是否是表
  isTable(origin:any):boolean{
    if( origin.type === this.objType.table )
      return true;
    else
      return false;
  }
  //#endregion

  //#region 判断是否是数据库
  isDatabase(origin){
    if( origin.type === this.objType.database )
      return true;
    else
      return false;
  }
  //endregion

  //#region 判断是否是字段
  isField(origin){
    if( origin.type === this.objType.field )
      return true;
    else
      return false;
  }
  //#endregion

  //#region 生成Angular模型类
  createAngularModel(key,prefix:string,suffix:string):string{
    let row = this.rs.filter(ele=>ele.auto_key === key)[0];
    let dbName = row["schema_name"];
    let tableName = row["table_name"];

    return this.createAngularModel_go(dbName,tableName,prefix,suffix);
  }
  //#endregion

  //#region 生成Mybatis批量保存语句
  createMybatisSaveBatch(key,upperAttribute?:boolean):string{
    let row = this.rs.filter(ele=>ele.auto_key === key)[0];
    let dbName = row["schema_name"];
    let tableName = row["table_name"];

    return this.createMybatisSaveBatch_go(dbName,tableName,upperAttribute);
  }
  //#endregion

  //#region 生成Mybatis保存单行数据
  /**
   * 根据 MySQL 表生成 Java 中的 Mybatis 保存+修改 当行数据的 SQL 语句。
   * @param key 查询数据库获取字段列表时手动添加的自增的序号
   * @param modelPackage Java后端项目中模型类的包名
   * @param upperAttribute 生成的 Mybatis SQL 语句中字段映射为的模型类的属性是否采用字段原始字母
   *              true 表示采用字段原始字母 - MySQL 中字段 is_test 在对应的 Java POJO 类
   *              中该属性也是 is_test，false 表示将 is_test 转换为 isTest作为类的属性
   */
  createMybatisSaveOne(key,modelPackage:string,upperAttribute?:boolean):string{
    let row = this.rs.filter(ele=>ele.auto_key === key)[0];
    let dbName = row["schema_name"];
    let tableName = row["table_name"];

    return this.createMybatisSaveOne_go(dbName,tableName,modelPackage,upperAttribute);
  }
  //#endregion

  /**
   * 根据选中节点返回所属数据库名称
   * @param key 树形结构选中节点的 key = this.rs.auto_key
   */
  getDBNameByKey(key:number):string{
    const row = this.rs.filter(ele=>ele.auto_key === key)[0];
    return row.schema_name;
  }

  /**
   * 根据选中节点返回表名称
   * @param key 树形结构选中节点的 key = this.rs.auto_key
   */
  getTableNameByKey(key:number):string{
    const row = this.rs.filter(ele=>ele.auto_key === key)[0];
    return row.table_name;
  }


  //#region 获取表字段数组中主键字段名称
  private getPrimaryKeyFieldName(fields:any[]){
    if( fields === undefined || fields === null || fields.length <= 0 ) return '';
    let ret = fields.filter(item => item.column_key + ''.toUpperCase() === 'PRI');
    if( ret.length<=0 ) return '';
    return ret[0].column_name;
  }
  //#endregion

  //#region 获取表主键字段在JAVA中的数据类型
  private getPrimaryKeyJavaDataType(fields:any[]){
    if( fields === undefined || fields === null || fields.length <= 0 ) return '';
    let ret = fields.filter(item => item.column_key + ''.toUpperCase() === 'PRI');
    if( ret.length<=0 ) return '';
    let dbDataType = ret[0].data_type + ''.toLowerCase();
    let javaDataType = this.dbDataType2JavaDataType[dbDataType];
    return javaDataType;
  }
  //#endregion

  //#region 获取表主键字段在Angular的数据类型
  private getPrimaryKeyAngularDataType(fields: any[]) {
    if (fields === undefined || fields === null || fields.length <= 0) {
      return '';
    }
    let ret = fields.filter(item => item.column_key + ''.toUpperCase() === 'PRI');
    if (ret.length <= 0) {
      return '';
    }
    let dbDataType = ret[0].data_type + ''.toLowerCase();
    let angularDataType = this.dataTypeMap[dbDataType];
    return angularDataType;
  }
  //#endregion

  //#region 生成 Mybatis 单行数据的新增/修改 SQL 语句
  private createMybatisSaveOne_go(dbName:string,tableName:string,
    modelPackage:string,upperAttribute?:boolean):string{

    if( this.rs.length<=0 ){
      console.log('没有缓存的树形原始记录集！');
      return;
    }

    let curRs = this.rs.filter(// 过滤获得指定表下的所有字段
      val => val.table_name === tableName && val.schema_name === dbName
    )

    if( curRs.length<=0 ) return '';

    let pk = this.getPrimaryKeyFieldName(curRs);
    let pkJavaDataType = this.getPrimaryKeyJavaDataType(curRs);

    let ret:string = `
<!-- 新增成功返回1，修改成功返回2 -->
<insert id="saveOne" parameterType="${modelPackage}">
    <!--<selectKey resultType="${pkJavaDataType}" keyProperty="${pk}" order="AFTER">
      SELECT LAST_INSERT_ID()
    </selectKey>-->

    insert into ${tableName} 
    <trim prefix="(" suffix=")" suffixOverrides=",">`

    curRs.forEach(ele => {
      // SQL 语句中的字段 - 保持原样
      let fieldName = ele["column_name"];

      // SQL 语句中的 POJO 类属性 - 可能要去掉下划线并且大写首字母
      let attri = upperAttribute === true?this.stringService.toPOJOAttribute(fieldName):fieldName;
      ret = ret + `
      <if test="${attri} != null"> ${fieldName}, </if>`
    });
    ret = ret + `
    </trim>
    values
    <trim prefix="(" suffix=")" suffixOverrides=",">`

    curRs.forEach(ele=>{
      // SQL 语句中的字段 - 保持原样
      let fieldName = ele["column_name"];

      // SQL 语句中的 POJO 类属性 - 可能要去掉下划线并且大写首字母
      let attri = upperAttribute === true?this.stringService.toPOJOAttribute(fieldName):fieldName;

      ret = ret + `
        <if test="${attri} != null"> #{${attri}},</if>`
    });
    ret = ret + `
    </trim>
    ON DUPLICATE KEY UPDATE
    <trim suffixOverrides=",">`

    curRs.forEach(item=>{
      // SQL 语句中的字段 - 保持原样
      let fieldName = item["column_name"];

      // SQL 语句中的 POJO 类属性 - 可能要去掉下划线并且大写首字母
      let attri = upperAttribute === true?this.stringService.toPOJOAttribute(fieldName):fieldName;

      let extra = item["extra"] === null?'':item["extra"];
      // 非自增列才修改数值
      if ( extra !=='auto_increment' ){
        ret = ret + `
        <if test="${attri} != null"> ${fieldName} = #{${attri}}, </if>`;
      }
    });

    ret = ret + `
    </trim>
</insert>`

    return ret;
  }
  //#endregion

  //#region 生成Mybatis批量保存代码
  private createMybatisSaveBatch_go(dbName:string,tableName:string,upperAttribute?:boolean):string{
    if( this.rs.length<=0 ){
      console.log('没有缓存的树形原始记录集！');
      return;
    }

    let curRs = this.rs.filter(// 过滤获得指定表下的所有字段
      val => val.table_name === tableName && val.schema_name === dbName
    )

    if( curRs.length<=0 ) return '';
    let pk = this.getPrimaryKeyFieldName(curRs);
    let pkJavaDataType = this.getPrimaryKeyJavaDataType(curRs);

    let ret:string = `
<!-- 批量写入时全部成功也只会返回1，中间有出现异常只影响后面的语句不影响之前的写入 -->
<insert id="saveBatch" parameterType="java.util.List">
    <!--<selectKey resultType="${pkJavaDataType}" keyProperty="${pk}" order="AFTER">
      SELECT LAST_INSERT_ID()
    </selectKey>-->

    <foreach collection ="list" item="ele" index= "index" separator =";">
    insert into ${tableName} 
    <trim prefix="(" suffix=")" suffixOverrides=",">`

    curRs.forEach(ele => {
      let fieldName = ele["column_name"];
      let attri = upperAttribute === true?this.stringService.toPOJOAttribute(fieldName):fieldName;

      ret = ret + `
      <if test="ele.${attri} != null"> ${fieldName}, </if>`
    });
    ret = ret + `
    </trim>
    values
    <trim prefix="(" suffix=")" suffixOverrides=",">`

    curRs.forEach(ele=>{
      let fieldName = ele["column_name"];
      let attri = upperAttribute === true?this.stringService.toPOJOAttribute(fieldName):fieldName;

      ret = ret + `
        <if test="ele.${attri} != null"> #{ele.${attri}},</if>`
    });
    ret = ret + `
    </trim>
    ON DUPLICATE KEY UPDATE
    <trim suffixOverrides=",">`

    curRs.forEach(item=>{
      let fieldName = item["column_name"];
      let attri = upperAttribute === true?this.stringService.toPOJOAttribute(fieldName):fieldName;
      let extra = item["extra"] === null?'':item["extra"];
      // 非自增列才修改数值
      if ( extra !=='auto_increment' ){
        ret = ret + `
        <if test="ele.${attri} != null"> ${fieldName} = #{ele.${attri}}, </if>`;
      }
    });

    ret = ret + `
    </trim>
    </foreach>
</insert>`

    return ret;
  }
  //#endregion

  //#region 创建指定表的 Angular Model
  private createAngularModel_go(dbName:string,tableName:string,prefix:string,suffix:string):string{
    if( this.rs.length<=0 ){
      console.log('没有缓存的树形原始记录集！');
      return;
    }

    let curRs = this.rs.filter(// 过滤获得指定表下的所有字段
      val => val.table_name === tableName && val.schema_name === dbName
    )


    let modelString = `// Author：chanchaw
// Generator Version：1.0 - 2019年11月16日 16点46分
// This Model Created：${new Date()}
// 记得导入UUID工具：import { UUID } from 'angular2-uuid';
export class ${ tableName }{`

    prefix = this.stringService.toRealString(prefix);
    suffix = this.stringService.toRealString(suffix);

    curRs.forEach(ele => {
      let dataType = this.dataTypeMap[ele.data_type];
      const preSuffixFieldName:string = prefix + ele["column_name"] + suffix;
      modelString = modelString + `
  ${preSuffixFieldName}:${dataType};`
    });

    modelString = modelString + `
  uuid:string;

  constructor(){
    this.uuid = UUID.UUID();
  }
}`

    return modelString;
  }
  //#endregion

  //#region 生成Mybatis selectAll代码
  createMybatisSelectAll(key):string{
    let row = this.rs.filter(ele=>ele.auto_key === key)[0];
    let dbName = row["schema_name"];
    let tableName = row["table_name"];

    return this.createMybatisSelectAll_go(dbName,tableName);
  }
  //#endregion

  //#region 生成Mybatis selectAll代码 - GO
  private createMybatisSelectAll_go(dbName:string,tableName:string):string{
    if( this.rs.length<=0 ){
      console.log('没有缓存的树形原始记录集！');
      return;
    }

    let curRs = this.rs.filter(// 过滤获得指定表下的所有字段
      val => val.table_name === tableName && val.schema_name === dbName
    )

    if( curRs.length<=0 ) return '';
    let pk = this.getPrimaryKeyFieldName(curRs);
    let ret = `<select id="selectAll" resultMap="BaseResultMap">`;
    ret = ret + `
    Select * From ${tableName} where 1=1 Order By seq,${pk}`;
    ret = ret + `
</select>`;
    return ret;
  }
  //#endregion

  //#region 生成Spring dao
  createSpringDao(key,modelName:string):string{
    let row = this.rs.filter(ele=>ele.auto_key === key)[0];
    let dbName = row["schema_name"];
    let tableName = row["table_name"];

    return this.createSpringDaoGo(dbName,tableName,modelName);
  }
  //#endregion

  //#region 生成Spring dao - GO
  createSpringDaoGo(dbName:string,tableName:string,modelName:string):string{
    if( this.rs.length<=0 ){
      console.log('没有缓存的树形原始记录集！');
      return;
    }

    let curRs = this.rs.filter(// 过滤获得指定表下的所有字段
      val => val.table_name === tableName && val.schema_name === dbName
    )

    let ret = `List<${modelName}> selectAll();
int saveOne(${modelName} record);
int saveBatch(List<${modelName}> list);`;
    return ret;
  }
  //#endregion

  //#region 生成 Spring 服务实现类
  createSpringServiceImpl(modelName:string):string{
    let ret = `
    @Service
    public class ${modelName}ServiceImpl implements ${modelName}Service {

      @Autowired
      private ${modelName}Mapper dao;

      @Override
      public int deleteByPrimaryKey(Integer iid) {
          return dao.deleteByPrimaryKey(iid);
      }

      @Override
      public ${modelName} insert(${modelName} record) {
        if( isExist(record.getSid()) == true)
          throw new BusinessException("编号：" + record.getSid() + ",已经存在！");
        int ret = dao.insert(record);
        return record;
      }

      @Override
      public ${modelName} insertSelective(${modelName} record) {
        if( isExist(record.getSid()) == true)
          throw new BusinessException("编号：" + record.getSid() + ",已经存在！");
        int ret = dao.insertSelective(record);
        return record;
      }

      @Override
      public ${modelName} selectByPrimaryKey(Integer iid) {
          return dao.selectByPrimaryKey(iid);
      }

      @Override
      public ${modelName} updateByPrimaryKeySelective(${modelName} record) {
          int ret = dao.updateByPrimaryKeySelective(record);
          return record;
      }

      @Override
      public ${modelName} updateByPrimaryKey(${modelName} record) {
          int ret = dao.updateByPrimaryKey(record);
          return record;
      }

      @Override
      public List<${modelName}> selectAll() {
          return dao.selectAll();
      }

      @Override
      public ${modelName} selectBySid(String sid) {
          return dao.selectBySid(sid);
      }
  
      @Override
      public boolean isExist(String sid) {
          if( sid == null || sid.length()<=0 ) return false;
          ${modelName} record = dao.selectBySid(sid);
          return record == null ? false:true;
      }

    }`

    return ret;
  }
  //#endregion

  /**
   * 返回添加了前后缀的字段构成的字符串
   * @param tableName MYSQL表名
   * @param prefix 原始字段添加前缀后形成目标字段
   * @param suffix 原始字段添加后缀后形成目标字段
   */
  createJavaModel(dbConfig:DBConfig,key:number,prefix:string,suffix:string):Observable<string>{
    const dbName:string = this.getDBNameByKey(key);
    const tableName:string = this.getTableNameByKey(key);

    if( tableName.length<=0 ) return of('');

    return this.getTableFields(dbConfig,dbName,tableName).pipe(map(
      ( res:MySQLColumnInfo[] ) =>{
        if( !res ) return '';

        // 将 undefined,null 转换为空字符串
        prefix = this.stringService.toRealString(prefix);
        suffix = this.stringService.toRealString(suffix);

        let fieldName:string = '';
        let javaDataType:string = '';
        let ret:string = ``;

        console.table(res);
        res.forEach(ele => {
          fieldName = ele.COLUMN_NAME;
          javaDataType = this.dbDataType2JavaDataType[ele.DATA_TYPE];

          if( prefix.length>0 ) fieldName = prefix + fieldName;
          if( suffix.length>0 ) fieldName = fieldName + suffix;
          
          ret = ret + `private ${javaDataType} ${fieldName};\n`;
        });
        return ret;
      }
    ));
  }

  //#region 生成Spring服务接口
  createSpringService(modelName:string):string{
    let ret = `
    public interface ${modelName}Service {
      int deleteByPrimaryKey(Integer iid);
  
      ${modelName} insert(${modelName} record);
  
      ${modelName} insertSelective(${modelName} record);
  
      ${modelName} selectByPrimaryKey(Integer iid);
  
      ${modelName} updateByPrimaryKeySelective(${modelName} record);
  
      ${modelName} updateByPrimaryKey(${modelName} record);

      List<${modelName}> selectAll();

      ${modelName} selectBySid(String sid);

      boolean isExist(String sid);

      // 将下面代码拷贝到MAPPER的XML文件中，记得修改表名
      /*
      <select id="selectBySid" parameterType="java.lang.String" resultMap="BaseResultMap">
        select * from storage where sid=#{sid,jdbcType=VARCHAR}
      </select>
      */

      // 将下面代码拷贝到DAO中
      // ${modelName} selectBySid(String sid);
    }`
    return ret;
  }
  //#endregion

  //#region 生成Spring 控制器
  createSpringController(key,modelName:string):string{
    let row = this.rs.filter(ele=>ele.auto_key === key)[0];
    let dbName = row["schema_name"];
    let tableName = row["table_name"];

    return this.createSpringControllerGo(dbName,tableName,modelName);
  }
  //#endregion

  //#region 生成Spring 控制器 - go
  createSpringControllerGo(dbName:string,tableName:string,modelName:string):string{
    if( this.rs.length<=0 ){
      console.log('没有缓存的树形原始记录集！');
      return;
    }

    let curRs = this.rs.filter(// 过滤获得指定表下的所有字段
      val => val.table_name === tableName && val.schema_name === dbName
    )

    let lowerModelName:string = modelName.substr(0,1).toLowerCase();
    lowerModelName = lowerModelName + modelName.substr(1);

    let pk = this.getPrimaryKeyFieldName(curRs);
    let pkJavaDataType = this.getPrimaryKeyJavaDataType(curRs);
    let ret = `
    @CrossOrigin
    @RestController
    @RequestMapping("/${lowerModelName}")
    public class ${modelName}Controller {
    
        @Autowired
        private ${modelName}Service service;
    
        @DeleteMapping("/deleteByPrimaryKey/{${pk}}")
        public JsonResult deleteByPrimaryKey(@PathVariable  ${pkJavaDataType} ${pk}){
            return JsonResult.ok(service.deleteByPrimaryKey(${pk}));
        }
    
        @GetMapping("/selectByPrimaryKey/{${pk}}")
        public JsonResult selectByPrimaryKey(@PathVariable ${pkJavaDataType} ${pk}){
            return JsonResult.ok(service.selectByPrimaryKey(${pk}));
        }
    
        @GetMapping("/selectAll")
        public JsonResult selectAll(){
            return JsonResult.ok(service.selectAll());
        }
    
        @PostMapping("/saveOne")
        public JsonResult saveOne(@RequestBody  ${modelName} record){
            return JsonResult.ok(service.saveOne(record));
        }
    
        @PostMapping("/saveBatch")
        public JsonResult saveBatch(@RequestBody List<${modelName}> list){
            return JsonResult.ok(service.saveBatch(list));
        }

        @PostMapping("/insert")
        public JsonResult insert(@RequestBody ${modelName} record){
            return JsonResult.ok(service.insertSelective(record));
        }

        @PutMapping("/update")
        public JsonResult update(@RequestBody ${modelName} record){
            return JsonResult.ok(service.updateByPrimaryKey(record));
        }

    
    }`;
    return ret;
  }
  //#endregion

  //#region 生成 Angular 服务
  createAngularService(key):string{
    let row = this.rs.filter(ele=>ele.auto_key === key)[0];
    let dbName = row["schema_name"];
    let tableName = row["table_name"];

    return this.createAngularServiceGo(dbName,tableName);
  }
  //#endregion

  //#region 生成 Angular Service
  /**
   * 生成 Angular Service
   * @param dbName 数据库名称
   * @param tableName 表名称
   */
  private createAngularServiceGo(dbName:string,tableName:string):string{
    if( this.rs.length<=0 ){
      console.log('没有缓存的树形原始记录集！');
      return;
    }

    let curRs = this.rs.filter(// 过滤获得指定表下的所有字段
      val => val.table_name === tableName && val.schema_name === dbName
    )

    let modelName:string = this.stringService.toUpperWords(tableName);
    let path_pre:string = modelName.charAt(0).toLowerCase() + modelName.slice(1);
    let pkFieldName:string = this.getPrimaryKeyFieldName(curRs);
    let pkFieldDataType:string = this.getPrimaryKeyAngularDataType(curRs);

    //#region 代码模板
    let ret = `
    private path_pre = '${path_pre}';

    private url = {
      'deleteByPrimaryKey':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre + '/deleteByPrimaryKey',
      'selectByPrimaryKey':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre + '/selectByPrimaryKey',
      'selectAll':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre + '/selectAll',
      'saveOne':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre,
      'insert':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre + '/insert',
      'update':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre + '/update',
    }
    constructor(
      private http:HttpClient
    ) { }
  
    deleteByPrimaryKey(${pkFieldName}:${pkFieldDataType}):Observable<JsonResult>{
      const url = this.url.deleteByPrimaryKey + '/' + ${pkFieldName};
      return this.http.delete<JsonResult>(url);
    }
  
    selectByPrimaryKey(${pkFieldName}:${pkFieldDataType}):Observable<JsonResult>{
      const url = this.url.selectByPrimaryKey + '/' + ${pkFieldName};
      return this.http.get<JsonResult>(url);
    }
  
    selectAll():Observable<JsonResult>{
      return this.http.get<JsonResult>(this.url.selectAll);
    }
  
    saveOne(record:${modelName}):Observable<JsonResult>{
      if( record === undefined || record === null ){
        let jr:JsonResult = new JsonResult();
        jr.code = 777;
        jr.msg = '数据为空，不可保存！';
        return of(jr);
      }
      if( record.iid === undefined || record.iid === null || record.iid <=0 )
        return this.insert(record);
      else
        return this.update(record);
    }
    
    private insert(record:${modelName}):Observable<JsonResult>{
      return this.http.post<JsonResult>(this.url.insert,record);
    }

    private update(record:${modelName}):Observable<JsonResult>{
      return this.http.put<JsonResult>(this.url.update,record);
    }

    `;
    //#endregion

    //#region 代码模板 - 2020年2月17日10:08:12新版本
    let ret217:string = `
import { Injectable } from '@angular/core';
import * as gc from 'src/app/global/const';
import { NzSort, NzFilterOpts, NzFilterReference, JsonResult, ZorroOption } from '../global/model';
import { HttpClient } from '@angular/common/http';
import { DatePipe } from '@angular/common';
import { ArrayutilsService } from './arrayutils.service';
import { Observable, of, forkJoin } from 'rxjs';
import { map } from 'rxjs/operators';
import { TableStyle, TableStyleService } from './table-style.service';
import { UUID } from 'angular2-uuid';
import { BreadFieldService } from './bread-field.service';

@Injectable({
  providedIn: 'root'
})
export class ${modelName}Service {

  //#region 公共属性
  private sortData:NzSort = null;
  private path_pre = '${path_pre}';
  private tableName='${tableName}';
  filterOpts:{ [fieldName:string]:NzFilterOpts[] } = {};
  filterMultiple:{ [fieldName:string]:boolean } = {};
  filterReference:NzFilterReference = new NzFilterReference();
  tableData:${modelName}[] = [];
  tableDataDisplay:${modelName}[] = [];
  tableStyle:TableStyle[] = [];
  editCache:{ [key:string]:{ edit:boolean,data:${modelName} } };// 编辑时的缓存数据
  enums:{ [key:string]:{label:string,value:string}[] } = {};
  emptyRowCount:number = 6;
  statistics:{ [key:string]:{ label:string,value:number } } = {};
  //#endregion

  //#region 请求URL
  private url = {
    'deleteByPrimaryKey':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre + '/deleteByPrimaryKey',
    'selectByPrimaryKey':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre + '/selectByPrimaryKey',
    'selectAll':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre + '/selectAll',
    'saveOne':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre,
    'insert':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre + '/insert',
    'update':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre + '/update',
    'restoreDeleted':gc.BACKEND_SERVER_ROOT + '/' + this.path_pre + '/restoreDeleted',
  }
  //#endregion

  //#region 构造方法 - 依赖注入
  constructor(
    private http:HttpClient,
    private datePipe:DatePipe,
    private arrUtils:ArrayutilsService,
    private tableStyleService:TableStyleService,
    private breadField:BreadFieldService,
  ) { }
  //#endregion

  //#region 删除行
  deleteByPrimaryKey(iid:number,rowIndex:number):Observable<JsonResult>{
    const url = this.url.deleteByPrimaryKey + '/' + iid;
    let record = this.tableDataDisplay.filter( item => item.iid === iid )[0];
    if( !record ) return of(JsonResult.getError('显示数据中未能获取到即将被删除的数据！'));
    let uuid:string = record.uuid;
    if( !uuid ) return of(JsonResult.getError('获取被删除数据的uuid失败！'));
    return this.http.delete<JsonResult>(url).pipe(map(
      ( jr:JsonResult ) =>{
        if( jr.code !==1 ) return jr;

        // 是作废模式的话，使用下面一行代码，相反则维持现状
        // this.nullifyRow_front(rowIndex,uuid);
        this.deleteRow_front(uuid);

        this.updateStatistics();
        return jr;
      }
    ));
  }
  //#endregion

  //#region 根据主键查询，加工：设置UUID，日期类型转换
  selectByPrimaryKey(iid:number):Observable<JsonResult>{
    const url = this.url.selectByPrimaryKey + '/' + iid;
    return this.http.get<JsonResult>(url).pipe(map(
      ( jr:JsonResult ) =>{
        if( jr.code !==1 ) return jr;
        let data:${modelName} = jr.data;
        if( data === undefined || data === null ) return jr;
        data.uuid = UUID.UUID();
        jr.data = this.transDate4Display(data);
        return jr;
      }
    ));
  }
  //#endregion

  //#region 查询获取所有数据后做如下操作：
  //1. 遍历数组设置UUID
  //2. 拷贝数组到tableDataDisplay
  selectAll():Observable<JsonResult>{
    return this.http.get<JsonResult>(this.url.selectAll).pipe(map(
      ( jr:JsonResult ) =>{
        if( jr.code !==1 ) return jr;
        let data:${modelName}[] = jr.data;
        if( data === undefined || data === null || data.length<=0 ) return jr;
        
        //1. 遍历数组设置UUID，同时转换日期类型
        data.forEach(ele => {
          ele.uuid = UUID.UUID();
          ele = this.transDate4Display(ele);
        });

        this.tableData = data;

        //3. 拷贝数据到 tableDataDisplay
        this.tableDataDisplay = {...data};

        jr.data = data;
        return jr;
      }

      
    ));
  }
  //#endregion

  //#region 保存一行数据
  saveOne(record:${modelName}):Observable<JsonResult>{
    if( record === undefined || record === null ){
      let jr:JsonResult = new JsonResult();
      jr.code = 777;
      jr.msg = '数据为空，不可保存！';
      return of(jr);
    }
    record = this.transDate4save(record);
    if( record.iid === undefined || record.iid === null || record.iid <=0 )
      return this.insert(record);
    else
      return this.update(record);
  }
  //#endregion
  
  //#region 插入一行数据
  private insert(record:${modelName}):Observable<JsonResult>{
    return this.http.post<JsonResult>(this.url.insert,record).pipe(map((jr:JsonResult) =>{
      if( jr.code !==1 ) return jr;
      let data:${modelName} = jr.data;
      if( data === undefined || data === null ) return jr;
      return jr;
    }));
  }
  //#endregion

  //#region 恢复被删除的数据
  /*
  restoreDeleted(iid:number,rowIndex:number):Observable<JsonResult>{
    const url = this.url.restoreDeleted + '/' + iid;
    return this.http.put<JsonResult>(url,null).pipe(map(
      ( res:JsonResult ) =>{
        if( res.code !==1 ) return res;
        this.tableData[rowIndex].state = 1;
        this.tableDataDisplay[rowIndex].state = 1;
        this.tableData = this.tableData.filter( item => 1===1 );
        this.tableDataDisplay = this.tableDataDisplay.filter( item => 1===1 );
        const uuid:string = this.tableData[rowIndex].uuid;
        this.editCache[uuid].data.state = 1;
        this.updateStatistics();
        return res;
      }
    ))
  }*/
  //#endregion

  //#region 修改一行数据
  private update(record:${modelName}):Observable<JsonResult>{
    return this.http.put<JsonResult>(this.url.update,record).pipe(map((jr:JsonResult) =>{
      if( jr.code !==1 ) return jr;
      let data:${modelName} = jr.data;
      if( data === undefined || data === null ) return jr;
      return jr;
    }))
  }
  //#endregion

  //#region 日期类型转换 - 为保存
  transDate4save(record:${modelName}):${modelName}{
    if( this.tableStyle === undefined || this.tableStyle === null || this.tableStyle.length<=0 ) return record;
    if( record === undefined || record === null ) return null;
    
    let dateTs:TableStyle[] = this.tableStyle.filter( item => item.date_formatter !== undefined && item.date_formatter !==null && item.date_formatter.length>0);
    if( dateTs.length<=0 ) return record;
    dateTs.forEach(ele => {
      if( record[ele.field_name] ){
        let date:Date = new Date(record[ele.field_name]);
        record[ele.field_name] = this.datePipe.transform(date,"yyyy-MM-dd HH:mm:ss");
      }
    });
    return record;
  }
  //#endregion

  //#region 日期类型转换 - 为显示
  transDate4Display(record:${modelName}):${modelName}{
    if( this.tableStyle === undefined || this.tableStyle === null || this.tableStyle.length<=0 ) return record;
    if( record === undefined || record === null ) return null;
    
    let dateTs:TableStyle[] = this.tableStyle.filter( item => item.date_formatter !== undefined && item.date_formatter !==null && item.date_formatter.length>0);
    if( dateTs.length<=0 ) return record;
    dateTs.forEach(ele => {
      let date:Date = new Date(record[ele.field_name]);
      record[ele.field_name] = this.datePipe.transform(date,ele.date_formatter);
    });
    return record;
  }
  //#endregion

  //#region 使用表格数据和样式数据构建用于过滤的下拉选项数组
  buildFilterOpts(td:${modelName}[],ts:TableStyle[]){
    if( !td || !ts ) return;
    for(let i=0;i<ts.length;i++){
      let ele = ts[i];
      let fieldName:string = ele.field_name;
      if( ele.filter === undefined || ele.filter === null || ele.filter.length<=0 ){
        continue;
      }
        

      let arr:${modelName}[] = this.arrUtils.buildDistArray(td,[ele.field_name]);
      if( arr ){
        let filterOpts:NzFilterOpts[] = [];
        arr.forEach(e1 => {
          let filterOptTextFieldName:string = ts[i].display_field;
          if( filterOptTextFieldName )
            filterOpts.push({text:e1[filterOptTextFieldName],value:e1[fieldName]});
          else
            filterOpts.push({text:e1[fieldName],value:e1[fieldName]});
        });

        this.filterOpts[fieldName] = filterOpts;

        // 设置过滤是否多选
        if( ts[i].filter === 'multiple' || ts[i].filter === 'input' )
          this.filterMultiple[ts[i].field_name] = true;
        if( ts[i].filter === 'single' )
          this.filterMultiple[ts[i].field_name] = false;
        
      }else{
        console.log('字段' + ts[i].field_name + '构建过滤用选项数组失败！');
      }
    }
  }
  //#endregion

  //#region 保存排序用参数，调用方法先过滤再排序
  sort(record:NzSort){
    this.sortData = record;
    this.filterGo();
  }
  //#endregion

  //#region 外部调用过滤方法
  filter(opts:string[],fieldName:string){
    this.filterReference = new NzFilterReference();
    this.filterReference.fieldName = fieldName;
    this.filterReference.opts = opts;
    this.filterGo();
  }
  //#endregion

  //#region 执行过滤
  filterGo(){
    let displayData:${modelName}[] = [];
    // 过滤
    displayData = this.tableData.filter(
      ( item:${modelName} ) =>{

        if( this.filterMultiple[this.filterReference.fieldName] === true ){// 多选过滤
          let fieldData:String = new String(item[this.filterReference.fieldName]);
          return this.filterReference.opts.length? (this.filterReference.opts.some(
            ( val:string ) => fieldData.indexOf(val) !==-1
          ) ?true:false):true;
        }

        if( this.filterMultiple[this.filterReference.fieldName] === false ){// 单选过滤
          let fieldData:String = new String(item[this.filterReference.fieldName]);
          let opt:string = <string>(this.filterReference.opts);
          return fieldData == opt;
        }

      }
    )

    if( this.filterReference.opts === null || this.filterReference.opts.length<=0 )
      displayData = this.tableData;
    

    this.tableDataDisplay = displayData;

    // 更新统计数据
    this.updateStatistics();

    // 无须排序
    if( this.sortData === undefined || this.sortData === null )
      return
    
      // 排序
    this.tableData = displayData.sort(
      (a,b) =>
      this.sortData.value === 'ascend'
      ? a[this.sortData.key!] > b[this.sortData.key!]
        ? 1
        : -1
      : b[this.sortData.key!] > a[this.sortData.key!]
      ? 1
      : -1
    );
    
  }
  //#endregion

  //#region 使用 forkJoin 同时获取数据和样式，并且初始化
  // 1. 编辑数据填充UUID，格式化日期类型字段的值
  // 2. 赋值 tableDataDisplay
  // 3. 构建用于过滤的下拉选项数组
  refresh():Observable<JsonResult>{
    const getData$ = this.http.get(this.url.selectAll);
    const getStyle$ = this.tableStyleService.selectByTableName(this.tableName);
    return forkJoin(getData$,getStyle$).pipe(map(
      ( res:any[] ) =>{
        if( !res ) return JsonResult.getError('请求服务器数据时出现异常！');
        let jrData:JsonResult = res[0];
        let jrStyle:JsonResult = res[1];
        if( !jrData || jrData.code !==1 ) return JsonResult.getError('获取数据时出现异常！');
        if( !jrStyle || jrStyle.code !==1 ) return JsonResult.getError('获取数据样式时出现异常！');

        // 成功获取数据和样式后，对数据进行必要的加工同时构建用于过滤的下拉选项数组
        // 1. 数据添加UUID，格式化日期类型
        // 2. 构建用于过滤的下拉选项数组
        jrData.data = this.resolveDataAndStyle(jrData.data,jrStyle.data);
        this.updateStatistics();
        return new JsonResult();
      }
    ))
  }
  //#endregion

  //#region 使用 table_style 表的设置修饰请求来的数据
  // 1. 为数据设置uuid
  // 2. 拷贝数据到tableDataDisplay
  // 3. 初始化编辑用缓存数据
  // 4. 构建过滤用的下拉数据源
  // 5. 构建枚举值
  // 6. 设置列宽、数字0显示为空字符串
  // 7. 最后返回修饰后的数据
  private resolveDataAndStyle(records:${modelName}[],ts:TableStyle[]):${modelName}[]{
    if( !records ) return null;
    if( !ts ) return records;

    let tsDate:TableStyle[] = ts.filter( item => item.date_formatter !== undefined && item.date_formatter !== null && item.date_formatter.length>0 );

    this.tableData = [];
    this.tableDataDisplay = [];
    // 遍历数据行，设置UUID，格式化日期类型字段
    records.forEach(dataRow => {
      dataRow.uuid = UUID.UUID();
      // dataRow.state = 1;// 设置获取到的所有数据为有效 2020年4月20日 17:02:45 弃用作废功能
      this.tableData.push({...dataRow});
      this.tableDataDisplay.push({...dataRow});
    });

    this.tableDataDisplay = this.tableDataDisplay.filter( item => 1===1 );

    // 给服务内属性赋值
    this.tableStyle = ts;
    this.updateEditCacheWithTableData();

    // 构建用于过滤的下拉选项数组
    this.buildFilterOpts(records,ts);

    // 遍历样式数组，设置列宽，构建枚举值
    ts.forEach(ele => {
      if( ele.data_type === 'number' ){// 数值0显示为空字符串
        this.tableDataDisplay.forEach(eleDD => {
          if( eleDD[ele.field_name] ===0 )
            eleDD[ele.field_name] = null;
        });
      }
      ele.width_percent_str = ele.width_percent + 'px';
      this.buildEnums(ele);// 构建枚举值
    });

    return records;
  }
  //#endregion

  //#region 构建枚举值
  buildEnums(tsRow:TableStyle){
    if( !tsRow ) return
    if( tsRow.ctl_type !=='enum' ) return
    
    let opts:string[] = new String(tsRow.opts).split(',');
    let optsArray:ZorroOption[] = [];
    opts.forEach(ele => {
      optsArray.push({ label:ele,value:ele });
    });
    this.enums[tsRow.field_name] = optsArray;
  }
  //#endregion

  //#region 使用表格数据更新编辑用缓存数据
  updateEditCacheWithTableData(){
    if( !this.tableData ) return
    this.editCache = {};
    this.tableData.forEach(ele => {
      this.editCache[ele.uuid] = { edit:false,data:{...ele} };
    });
  }
  //#endregion

  //#region 从编辑缓存中获取所有处于编辑状态的行构建为数组返回
  getEdited():${modelName}[]{
    let ret:${modelName}[] = [];
    for(let attr in this.editCache){
      if( this.editCache[attr].edit === true ){
        ret.push({...this.editCache[attr].data});
      }
    }

    return ret;
  }
  //#endregion

  //#region 初始化来自基础资料的下拉数据源
  initOpts():Observable<JsonResult>{
    // const cashType$ = this.cashTypeService.selectAll();
    // const cc$ = this.ccService.selectAll();

    // return forkJoin( cashType$,cc$ ).pipe(map(
    //   ( res:any[] ) =>{

    //     if( !res ) JsonResult.getError('初始化下拉数据时出现异常！');
    //     let ddCashType:JsonResult = res[0];
    //     let ddCC:JsonResult = res[1];

    //     this.cashType = ddCashType.data ? ddCashType.data:null;
    //     this.cc = ddCC.data ? ddCC.data:null;

    //     return new JsonResult();
    //   }
    // ))
    return of(new JsonResult());
  }
  //#endregion

  //#region 新增空行
  addEmptyRow(){
    let record:${modelName} = new ${modelName}();
    let uuid:string = record.uuid;

    this.setDefaultValue(record);// 设置默认值
    this.addEmptyRowAtHeader(record);

    this.editCache[uuid] = {
      edit:true,data:{...record}
    };
  }
  //#endregion

  //#region 设置默认值
  private setDefaultValue(record:${modelName}){
    if( !this.tableStyle ) return;
    this.tableStyle.forEach(ele => {
      if( ele.default_value ){
        switch(ele.data_type){
          case 'date':
            let val:number = Date.parse(new Date().toString()) + parseInt(ele.default_value);
            record[ele.field_name] = new Date(val);
            break;
          default:
            record[ele.field_name] = ele.default_value;
        }
      }
    });
  }
  //#endregion

  //#region 在数组头上添加空行
  addEmptyRowAtHeader(record:${modelName}){
    this.tableData = this.tableData.filter( item => 1===1 );
    this.tableData.splice(0,0,{...record});

    this.tableDataDisplay = this.tableDataDisplay.filter( item => 1===1 );
    this.tableDataDisplay.splice(0,0,{...record});
  }
  //#endregion

  //#region 在数组尾部添加空行
  addEmptyRowAtFooter(record:${modelName}){
    this.tableData = this.tableData.concat({...record});
    this.tableDataDisplay = this.tableDataDisplay.concat({...record});
  }
  //#endregion

  //#region 保存编辑：新增后的编辑 + 修改的编辑
  saveEdit(rowIndex:number,uuid:string):Observable<JsonResult>{
    let record:${modelName} = this.editCache[uuid].data;

    return this.saveOne(record).pipe(map(
      ( jr:JsonResult ) =>{

        if( jr.code !==1 ) return jr;

        let newData:${modelName} = jr.data;
        newData.uuid = UUID.UUID();
        
        this.tableData = this.arrUtils.replace<${modelName}>(this.tableData,rowIndex,newData);
        this.tableDataDisplay = this.arrUtils.replace<${modelName}>(this.tableData,rowIndex,newData);

        // 之前的方法不能兼容多行同时编辑
        // 多行都处于编辑状态时，保存一行，其他行都会恢复为编辑前数据和状态
        this.updateEditCacheOneRow(newData);

        return jr;
      }
    ))
  }
  //#endregion

  //#region 只更新当前保存的一行，兼容多行同时编辑
  updateEditCacheOneRow(data:${modelName}){
    this.editCache[data.uuid] = { edit:false,data:{...data} };
  }
  //#endregion

  //#region 删除一行数据
  deleteRow(rowIndex:number,uuid:string):Observable<JsonResult>{
    if( rowIndex === undefined || rowIndex === null || rowIndex<0 ) return of(new JsonResult);
    if( !uuid) return of(new JsonResult);
    let iid:number = this.tableDataDisplay[rowIndex].iid;
    if( !iid ) {
      this.updateStatistics();
      return of(new JsonResult);
    }

    return this.deleteByPrimaryKey(iid,rowIndex);
  }
  //#endregion

  //#region 前端删除行
  deleteRow_front(uuid:string){
    this.tableData = this.tableData.filter( item => item.uuid !==uuid );
    this.tableDataDisplay = this.tableDataDisplay.filter( item => item.uuid !==uuid );
    this.updateEditCacheWithTableData();
  }
  //#endregion

  //#region 前端作废一行数据
  nullifyRow_front(rowIndex:number,uuid:string){
    /* 2020年4月20日 16:59:06 弃用
    this.tableData[rowIndex].state = 0;
    this.tableDataDisplay[rowIndex].state = 0;
    this.editCache[uuid].data.state = 0;

    this.tableDataDisplay = this.tableDataDisplay.filter( item => 1===1 );
    */
  }
  //#endregion

  //#region 设置对齐方式
  colHeaderAlign(fieldName:string):string{
    if( !this.tableStyle ) return;
    let tsRow:TableStyle = this.tableStyle.filter( item => item.field_name === fieldName )[0];
    return tsRow.align_header;
  }

  colContentAlign(fieldName:string):string{
    if( !this.tableStyle ) return;
    let tsRow:TableStyle = this.tableStyle.filter( item => item.field_name === fieldName )[0];
    return tsRow.align_cell;
  }
  //#endregion

  //#region 复制行
  copyRow(rowIndex:number,uuid:string){
    let newRow:${modelName} = {...this.tableData[rowIndex]};
    newRow.uuid = UUID.UUID();
    newRow.iid = 0;

    this.tableData = this.tableData.concat({...newRow});
    this.tableDataDisplay = this.tableData;
    this.updateEditCacheWithTableData();
  }
  //#endregion

  //#region 变形后显示
  convertRaw2Show(record:${modelName},tsRow:TableStyle):string{
    // switch(tsRow.field_name){
    //   case 'company_sid':
    //     return record.company_name;
    //   default:
    //     return this.breadField.transRaw2Display(record,tsRow);
    // }
    return '';
  }
  //#endregion

  //#region 更新统计数据
  updateStatistics(){
    this.statistics = null;
    this.statistics = {};
    if( !this.tableStyle ) return; // 没有获取到样式表数据则不初始化统计数据
    this.tableStyle.forEach(ele => {
      if( ele.statistics ){
        this.statistics[ele.field_name] = { value:0,label:'0' };
      }
    });

    let staArray:TableStyle[] = this.tableStyle.filter( item => item.statistics );

    if( !this.tableData ) return;
    this.tableDataDisplay.forEach(eleData => {
      staArray.forEach(eleStyle => {
        let fieldName:string = eleStyle.field_name;
        let data:number = eleData[fieldName];
        
        data = data === undefined ? 0:data;
        data = data === null ? 0:data;

        // 被删除的数据（无效数据）不参与统计
        /* 2020年4月20日 16:59:45 弃用
        if( eleData['state'] ===0 )
          data = 0;
        */
        let addResult:number = data + this.statistics[fieldName].value;

        this.statistics[fieldName] = { value: addResult,label: addResult.toFixed(eleStyle.digit)};

      });
    });
  }
  //#endregion

  //#region 开启编辑状态
  startEdit(uuid:string):string{
    /* 2020年4月20日 17:00:07 弃用
    if( this.editCache[uuid].data.state !==1 ){
      return '无效数据不可修改！';
    }
    */
    this.editCache[uuid].edit = true;
    return '';
  }
  //#endregion

}`

    const model:string = this.createAngularModel_go(dbName,tableName,'','');
    ret217 = ret217 + model;
    //#endregion
    return ret217;
  }
  //#endregion

  //#region 生成表字段构成的字符串，默认以逗号间隔
  createFieldString(key):string{
    let row = this.rs.filter(ele=>ele.auto_key === key)[0];
    let dbName = row["schema_name"];
    let tableName = row["table_name"];
    let delimiter = ',';

    if( dbName.length<=0 ) return '';
    if( tableName.length<=0 ) return '';

    let curRs = this.rs.filter(// 过滤获得指定表下的所有字段
      val => val.table_name === tableName && val.schema_name === dbName
    )

    if( curRs.length<=0 ) return '';
    let ret:string = '';
    curRs.forEach(ele => {
      ret = ret + ele.column_name + delimiter;
    });

    if( ret.substr(ret.length-1) === delimiter ) ret = ret.substr(0,ret.length-1);
    return ret;
  }
  //#endregion

  //#region 生成单表类型的模板文件代码
  createSingleTemplate():string{

    //#region 代码模板 - 主要逻辑做在组件中
    let ret:string = `
  <!-- 
  方法说明：
  addEmptyRow：新增一个空行
  refreshTable：刷新表格数据，从远端读取数据更新到本地显示。
  colHeaderAlign：根据字段名返回该字段列头的对齐方式
  startEdit：开启一行的编辑模式

  属性说明：
  vari.tableData：表格的数据源
  vari.tableStyle:业务数据表在样式表 table_style 中的设置的参数的集合，一行是针对业务表一个字段的配置
  -->
<div>
  <a (click)="addEmptyRow()"><i nz-icon nzType="plus" nzTheme="outline"></i>&nbsp;新增行</a>&nbsp;
  <a (click)="refreshTable()"><i nz-icon nzType="sync" nzTheme="outline"></i>&nbsp;刷新</a>
</div>&nbsp;
<nz-table #zorroTable nzBordered [nzData]="vari.tableData" nzSize="small">
  <thead>
    <tr class="strip" style="background-color: #1890ff;">
      <th nzWidth="6%">#</th><!-- 序号列 -->
      <ng-container *ngFor="let item of vari.tableStyle"><!-- 遍历样式表构建列头，不创建immutable列 -->
        <th *ngIf="!item.immutable" [nzWidth]="item.width_percent_str" 
          [nzAlign]="colHeaderAlign(item.field_name)">
          {{ item.text }}
        </th>
      </ng-container>
    </tr>
  </thead>

  <tbody>
    <!-- 
      class.strip 是斑马纹，将下面的样式代码拷贝到css文件中
      /* 表格组件斑马纹 */
      .strip{
          background-color: #eff4f9;
      }
    -->
    <tr *ngFor="let rowData of zorroTable.data;let rowIndex=index" (dblclick)="startEdit(rowIndex,rowData['uuid'],$event)"
      (mouseover)="mouseOver(mouseOverUUID = rowData.uuid)" [class.strip]="rowIndex%2 !== 0">
      <td><!-- 序号 + 图标列 -->
        <!-- 序号列 - 显示状态下的两个图标：删除、复制 -->
        <div *ngIf="!cache.detail[rowData['uuid']].edit;else editIconTpl">
          <div *ngIf="rowData.uuid === mouseOverUUID;else rowIndexTpl" nz-row nzType="flex" nzJustify="space-between">
            <div nz-col nzSpan="10">
              <a 
                nz-popconfirm
                nzPopconfirmTitle="您确定要删除当前行?"
                (nzOnConfirm)="deleteRow(rowIndex,rowData['uuid'])"
                (nzOnCancel)="cancelDeleteRow()"
                nzPopconfirmPlacement="top"
              ><i nz-icon nzType="delete" nzTheme="outline"></i></a>
            </div>

            <div nz-col nzSpan="10">
              <a 
                (click)="copyRow(rowIndex,rowData['uuid'])"><i nz-icon nzType="copy" nzTheme="outline"></i></a>
                <!-- 由于空间有限只能放两个图标，一开始是：删除、编辑，后来修改为：删除、复制，通过双击行开启编辑状态 -->
              <!-- <a (click)="startEdit(rowIndex,rowData['uuid'])"><i nz-icon nzType="edit" nzTheme="outline"></i></a> -->
            </div>
          </div>
          <ng-template #rowIndexTpl>{{rowIndex+1}}</ng-template>
        </div>

        <!-- 序号列 - 编辑状态下的两个图标：取消、保存 -->
        <ng-template #editIconTpl>
          <div nz-row nzType="flex" nzJustify="space-between">
            <div nz-col nzSpan="10"><!-- 取消编辑 nz-tooltip nzTooltipTitle="取消编辑" -->
              <a 
                (click)="cancelEdit(rowIndex,rowData['uuid'])"><i nz-icon nzType="redo" nzTheme="outline"></i></a>
            </div>


            <div nz-col nzSpan="10"><!-- 保存 nz-tooltip nzTooltipTitle="保存" -->
              <a 
                (click)="saveEdit(rowIndex,rowData['uuid'])"><i nz-icon nzType="save" nzTheme="outline"></i></a>
            </div>
          </div>
        </ng-template>
      </td>

      <!-- 序号之后的所有数据列 -->
      <ng-container *ngFor="let tsRow of vari.tableStyle">
        <td *ngIf="!tsRow.immutable" [nzAlign]="colContentAlign(tsRow.field_name)">
          <!-- 显示模式 -->
          <ng-container *ngIf="!cache.detail[rowData['uuid']].edit;else editTpl">

            <ng-container *ngIf="tsRow.date_pipe;else plainShowTpl">
              {{ rowData[tsRow.field_name] | date:tsRow.date_formatter }}
            </ng-container>
            <ng-template #plainShowTpl>
              {{ convertRaw2Show(rowData,tsRow) }}
            </ng-template>

          </ng-container>
          
          <ng-template #editTpl><!-- 编辑模式，使用 switch 枚举用于编辑的控件类型 -->

            <span [ngSwitch]="tsRow.ctl_type">
              <p *ngSwitchCase="'optsEnum'"><!-- 下拉选择 - 枚举 -->
                <nz-select nzShowSearch nzAllowClear nzPlaceHolder="" [(ngModel)]="cache.detail[rowData.uuid].data[tsRow.field_name]" style="width: 100%;">
                  <nz-option *ngFor="let item of opts_enum[tsRow.field_name]" [nzLabel]="item.label" [nzValue]="item.value"></nz-option>
                </nz-select>
              </p>

              <p *ngSwitchCase="'date'"><!-- 日期控件  [nzFormat]="'yyyy-MM-dd HH:mm:ss'" -->
                <nz-date-picker [nzFormat]="'yyyy-MM-dd'" [(ngModel)]="cache.detail[rowData.uuid].data[tsRow.field_name]"></nz-date-picker>
              </p>

              <p *ngSwitchCase="'input'">
                <input type="text" nz-input [(ngModel)]="cache.detail[rowData.uuid].data[tsRow.field_name]" 
                (ngModelChange)="inputChange(cache.detail[rowData.uuid].data,tsRow)"/>
              </p>

              <p *ngSwitchCase="'locked'"><!-- 被锁定 -->
                <!-- {{ cache.detail[rowData.uuid].data[tsRow.field_name] }} -->
                <!-- {{ resolveFormula(rowData,tsRow) }} -->
                {{ breadField.showLocked(rowData,tsRow) }}
              </p>

              <p *ngSwitchDefault><!-- 默认 input，这里一般手动制作下拉多选或者下拉单选，数据源为请求远端获取，在表 table_style.ctl_type 中填写：custome -->
                <input type="text" nz-input [(ngModel)]="cache.detail[rowData.uuid].data[tsRow.field_name]" 
                  (ngModelChange)="inputChange(cache.detail[rowData.uuid].data,tsRow)"/>

                  <!-- 下拉多选 nzMode有三种模式：multiple，tags，default
                  <ng-container *ngIf="tsRow.field_name == 'type_sid'">
                      <nz-select style="width: 100%;"
                      [nzMaxTagCount]="2"
                      [nzMaxTagPlaceholder]="tagPlaceHolder"
                      nzMode="multiple"
                      nzPlaceHolder="可以多选"
                      [(ngModel)]="cache.detail[rowData.uuid].data['type_sid_array']"
                      >
                          <nz-option *ngFor="let option of opts_req['type_sid']" [nzLabel]="option.name" [nzValue]="option.sid"></nz-option>
                      </nz-select>

                      <ng-template #tagPlaceHolder let-selectedList> ... </ng-template>
                  </ng-container>

                  下拉单选，输入后可搜索
                  <nz-select *ngIf="tsRow.field_name == 'settlement_sid'" style="width: 100%;"
                      nzShowSearch nzAllowClear nzPlaceHolder="" [(ngModel)]="cache.detail[rowData.uuid].data['settlement_sid']">
                      <nz-option *ngFor="let option of opts_settlement" [nzLabel]="option.company_name" [nzValue]="option.sid"></nz-option>
                  </nz-select>
                  -->

              </p>

            </span>

          </ng-template>

        </td>
      </ng-container>

    </tr>

    <!-- 统计 -->
    <!-- <tr>
      <td>合计：</td>
      <ng-container *ngFor="let tsRow of vari.tableStyle">
        <td *ngIf="!tsRow.immutable" [nzAlign]="'right'">
          <ng-container *ngIf="tsRow.statistics;else elseBlock">
            {{ vari.statistics[tsRow.field_name]?.label }}
          </ng-container>
          
          <ng-template #elseBlock></ng-template>
        </td>
      </ng-container>
      
    </tr> -->
  </tbody>

</nz-table>
`
//#endregion

    // 2020年2月17日09:45:09整理后采用下面的代码模板
    // 之前业务逻辑都写在组件中，之后业务逻辑都写在服务中
    // 组件中只处理必要的和模板相关的逻辑
    //#region 代码模板 - 主要逻辑做在服务中 - 2020年2月17日09:16:47
    const ret217:string = `
    <!-- 
  方法说明：
  addEmptyRow：新增一个空行
  refreshTable：刷新表格数据，从远端读取数据更新到本地显示。
  colHeaderAlign：根据字段名返回该字段列头的对齐方式
  startEdit：开启一行的编辑模式

  属性说明：
  vari.tableData：表格的数据源
  vari.tableStyle:业务数据表在样式表 table_style 中的设置的参数的集合，一行是针对业务表一个字段的配置
  -->
  <!-- 将下面代码拷贝到 CSS 中 
  .strip{
    background-color: #eff4f9;
}

.delLine{
    text-decoration: line-through;
}
-->

<div>
    <a (click)="addEmptyRow()"><i nz-icon nzType="plus" nzTheme="outline"></i>&nbsp;新增行</a>&nbsp;
    <a (click)="refresh()"><i nz-icon nzType="sync" nzTheme="outline"></i>&nbsp;刷新</a>
</div>&nbsp;
<nz-table #zorroTable nzBordered [nzData]="detailService.tableDataDisplay" nzSize="small" [nzPageSize]="'1000'">
    <thead>
        <tr class="strip" style="background-color: #f4f4f4;">
            <th nzWidth="60px">#</th>
            <!-- 序号列 -->
            <ng-container *ngFor="let tsRow of detailService.tableStyle">

                <!-- 遍历样式表构建列头，不创建immutable列 -->
                <ng-container *ngIf="!tsRow.immutable">
                    <ng-container [ngSwitch]="tsRow.filter">
                        
                        <th *ngSwitchCase="'multiple'" [nzWidth]="tsRow.width_percent_str" [nzAlign]="detailService.colHeaderAlign(tsRow.field_name)" 
                            nzShowFilter [nzFilters]="detailService.filterOpts[tsRow.field_name]"
                            [nzFilterMultiple]="true"
                            (nzFilterChange)="filter($event, tsRow.field_name)">
                            {{ tsRow.text }}
                        </th>

                        <th *ngSwitchCase="'single'" [nzWidth]="tsRow.width_percent_str" [nzAlign]="detailService.colHeaderAlign(tsRow.field_name)" 
                            nzShowFilter [nzFilters]="detailService.filterOpts[tsRow.field_name]" 
                            [nzFilterMultiple]="false"
                            (nzFilterChange)="filter($event, tsRow.field_name)">
                            {{ tsRow.text }}
                        </th>
                        
                        <th *ngSwitchCase="'input'" nzCustomFilter>
                          {{ tsRow.text }}
                          <i nz-th-extra class="ant-table-filter-icon" nz-icon nz-dropdown
                            #dropdown="nzDropdown"
                            nzType="search"
                            [nzDropdownMenu]="inputFilterMenu"
                            [class.ant-table-filter-open]="dropdown.nzVisible"
                            nzTrigger="click"
                            nzPlacement="bottomRight"
                            [nzClickHide]="false"
                            nzTableFilter
                          ></i>
                        </th>

                        <th *ngSwitchDefault [nzWidth]="tsRow.width_percent_str" [nzAlign]="detailService.colHeaderAlign(tsRow.field_name)">
                            {{ tsRow.text }}
                        </th>

                        <!-- 手动输入模糊查询的下拉菜单模板 -->
                        <nz-dropdown-menu #inputFilterMenu="nzDropdownMenu">
                            <div class="search-box">
                                <input type="text" nz-input placeholder="输入模糊查询" [(ngModel)]="vari.searchInputValue" />
                                <button nz-button nzSize="small" nzType="primary" (click)="searchInput()" class="search-button">
                                过滤
                                </button>
                                <button nz-button nzSize="small" (click)="resetSearchInput()">重置</button>
                            </div>
                        </nz-dropdown-menu>
                        
                    </ng-container>

                </ng-container>
            </ng-container>
        </tr>
    </thead>

    <tbody>
        <!-- 
        class.strip 是斑马纹，将下面的样式代码拷贝到css文件中
        /* 表格组件斑马纹 */
        .strip{
            background-color: #eff4f9;
        }

        2020年4月20日 17:00:44 弃用作废功能
        [class.delLine]=" rowData.state === 0 "
      -->
        <tr *ngFor="let rowData of zorroTable.data;let rowIndex=index" 
            (dblclick)="startEdit(rowIndex,rowData['uuid'],$event)" 
            (mouseover)="mouseOver(mouseOverUUID = rowData.uuid)" [class.strip]="rowIndex%2 !== 0"
            >
            <td>
                <!-- 序号/图标列 -->
                <div *ngIf="!detailService.editCache[rowData['uuid']].edit;else editIconTpl">
                    <!-- 鼠标悬停的图标 -->
                    <div *ngIf="rowData.uuid === mouseOverUUID;else rowIndexTpl" nz-row nzType="flex" nzJustify="space-between">
                        <!-- 删除/恢复图标 -->
                        <div nz-col nzSpan="10">
                            <a nz-popconfirm nzPopconfirmTitle="您确定要删除当前行?" (nzOnConfirm)="deleteRow(rowIndex,rowData['uuid'])" (nzOnCancel)="cancelDeleteRow()" nzPopconfirmPlacement="top"><i nz-icon nzType="close" nzTheme="outline"></i></a>
                            <!-- 
                              2020年4月20日 17:01:21 弃用作废功能 
                              <a *ngIf="rowData['state']===1;else deletedRowTmpl" nz-popconfirm nzPopconfirmTitle="您确定要删除当前行?" (nzOnConfirm)="deleteRow(rowIndex,rowData['uuid'])" (nzOnCancel)="cancelDeleteRow()" nzPopconfirmPlacement="top"><i nz-icon nzType="close" nzTheme="outline"></i></a>
                            
                              <ng-template #deletedRowTmpl>
                                  <a nz-popconfirm nzPopconfirmTitle="您确定要恢复当前行?" (nzOnConfirm)="restoreDeleted(rowIndex,rowData['uuid'])" (nzOnCancel)="cancelDeleteRow()" nzPopconfirmPlacement="top"><i nz-icon nzType="sync" nzTheme="outline"></i></a>
                              </ng-template>
                            -->
                        </div>

                        <!-- 复制图标 -->
                        <div nz-col nzSpan="10">
                            <a (click)="detailService.copyRow(rowIndex,rowData['uuid'])"><i nz-icon nzType="copy" nzTheme="outline"></i></a>
                            <!-- 由于空间有限只能放两个图标，一开始是：删除、编辑，后来修改为：删除、复制，通过双击行开启编辑状态 -->
                            <!-- <a (click)="startEdit(rowIndex,rowData['uuid'])"><i nz-icon nzType="edit" nzTheme="outline"></i></a> -->
                        </div>
                    </div>

                    <!-- 序号 -->
                    <ng-template #rowIndexTpl>{{rowIndex+1}}</ng-template>
                </div>

                <!-- 序号列 - 编辑状态下的两个图标：取消、保存 -->
                <ng-template #editIconTpl>
                    <div nz-row nzType="flex" nzJustify="space-between">
                        <div nz-col nzSpan="10">
                            <!-- 取消编辑 nz-tooltip nzTooltipTitle="取消编辑" -->
                            <a (click)="cancelEdit(rowIndex,rowData['uuid'])"><i nz-icon nzType="redo" nzTheme="outline"></i></a>
                        </div>


                        <div nz-col nzSpan="10">
                            <!-- 保存 nz-tooltip nzTooltipTitle="保存" -->
                            <a (click)="saveEdit(rowIndex,rowData['uuid'])"><i nz-icon nzType="save" nzTheme="outline"></i></a>
                        </div>
                    </div>
                </ng-template>
            </td>

            <!-- 序号之后的所有数据列 -->
            <ng-container *ngFor="let tsRow of detailService.tableStyle">
                <td *ngIf="!tsRow.immutable" [nzAlign]="detailService.colContentAlign(tsRow.field_name)">
                    <!-- 显示模式 -->
                    <ng-container *ngIf="!detailService.editCache[rowData['uuid']].edit;else editTpl">

                        <!-- 显示格式多分支 -->
                        <span [ngSwitch]="tsRow.use_formatter">
                            <div *ngSwitchCase="'date'">
                                {{ rowData[tsRow.field_name] | date:tsRow.date_formatter }}
                            </div>

                            <div *ngSwitchCase="'digit'">
                                {{ rowData[tsRow.field_name] | number:tsRow.digit_formatter }}
                            </div>

                            <div *ngSwitchCase="'field'">
                                {{ rowData[tsRow.display_field] }}
                            </div>
                            
                            <!-- 仅显示勾选 -->
                            <div *ngSwitchCase="'onlyTick'">
                              <span *ngIf="rowData[tsRow.field_name]">√</span>
                            </div>
  
                            <!-- 仅显示叉 -->
                            <div *ngSwitchCase="'onlyCross'">
                              <span *ngIf="!rowData[tsRow.field_name]">×</span>
                            </div>
  
                            <!-- 同时显示勾选和叉 -->
                            <div *ngSwitchCase="'twoWay'">
                              <span *ngIf="rowData[tsRow.field_name];else showCross">√</span>
                              <ng-template #showCross>×</ng-template>
                            </div>
                            
                            <!-- 库存模式 -->
                            <div *ngSwitchCase="'inventory'">
                              <span [ngSwitch]="rowData[tsRow.field_name]">
                                <div *ngSwitchCase="'1'">增加</div>
                                <div *ngSwitchCase="'-1'">减少</div>
                                <div *ngSwitchCase="'0'">不影响</div>
                              </span>
                            </div>

                            <div *ngSwitchDefault>
                                {{ rowData[tsRow.field_name] }}
                            </div>
                        </span>

                    </ng-container>

                    <!-- 编辑模式 -->
                    <ng-template #editTpl>
                        <!-- 枚举编辑的控件类型，多分支 -->
                        <span [ngSwitch]="tsRow.ctl_type">
                            <p *ngSwitchCase="'optsEnum'"><!-- 键值对类型的枚举 - 已弃用 -->
                                <span *ngIf="tsRow.field_name === 'colpay';else otherOptsEnumBlock">
                                    <nz-select nzShowSearch nzPlaceHolder="" [(ngModel)]="detailService.editCache[rowData.uuid].data[tsRow.field_name]" style="width: 100%;"
                                        (ngModelChange)="onChange_category($event)">
                                        <nz-option *ngFor="let item of detailService.enums[tsRow.field_name]" [nzLabel]="item.label" [nzValue]="item.value"></nz-option>
                                    </nz-select>
                                </span>

                                <ng-template #otherOptsEnumBlock>
                                    <nz-select nzShowSearch nzPlaceHolder="" [(ngModel)]="detailService.editCache[rowData.uuid].data[tsRow.field_name]" style="width: 100%;">
                                        <nz-option *ngFor="let item of detailService.enums[tsRow.field_name]" [nzLabel]="item.label" [nzValue]="item.value"></nz-option>
                                    </nz-select>
                                </ng-template>
                            </p>

                            <!-- 日期控件  [nzFormat]="'yyyy-MM-dd HH:mm:ss'" -->
                            <p *ngSwitchCase="'date'">
                                <nz-date-picker [nzFormat]="'yyyy-MM-dd'" [(ngModel)]="detailService.editCache[rowData.uuid].data[tsRow.field_name]"></nz-date-picker>
                            </p>

                            <!-- 枚举类型：现金，记账，支票，承兑 -->
                            <p *ngSwitchCase="'enum'">
                                <nz-select nzPlaceHolder="" [(ngModel)]="detailService.editCache[rowData.uuid].data[tsRow.field_name]" style="width: 100%;"
                                    (ngModelChange)="onChange_enum($event,tsRow.field_name)">
                                    <nz-option *ngFor="let item of detailService.enums[tsRow.field_name]" [nzLabel]="item.label" [nzValue]="item.value"></nz-option>
                                </nz-select>
                            </p>

                            <p *ngSwitchCase="'input'">
                                <input type="text" nz-input [(ngModel)]="detailService.editCache[rowData.uuid].data[tsRow.field_name]" (ngModelChange)="inputChange(detailService.editCache[rowData.uuid].data,tsRow)" />
                            </p>

                            <p *ngSwitchCase="'locked'">
                                <!-- 被锁定 -->
                                <!-- {{ detailService.editCache[rowData.uuid].data[tsRow.field_name] }} -->
                                <!-- {{ resolveFormula(rowData,tsRow) }} -->
                                {{ breadField.showLocked(rowData,tsRow) }}
                            </p>

                            <!-- 默认 input，这里一般手动制作下拉多选或者下拉单选，数据源为请求远端获取，在表 table_style.ctl_type 中填写：custome -->
                            <p *ngSwitchDefault>
                                <nz-select *ngIf="tsRow.field_name == 'category'" style="width: 100%;" nzShowSearch nzPlaceHolder="" [(ngModel)]="detailService.editCache[rowData.uuid].data['category']">
                                    <nz-option *ngFor="let option of detailService.cashType" [nzLabel]="option.sid" [nzValue]="option.sid"></nz-option>
                                </nz-select>

                                <nz-select *ngIf="tsRow.field_name == 'company_sid'" style="width: 100%;" nzShowSearch nzPlaceHolder="" [(ngModel)]="detailService.editCache[rowData.uuid].data['company_sid']">
                                    <nz-option *ngFor="let option of detailService.cc" [nzLabel]="option.company_name" [nzValue]="option.sid"></nz-option>
                                </nz-select>
                            </p>

                        </span>

                    </ng-template>

                </td>
            </ng-container>

        </tr>

        <!-- 统计 -->
        <tr>
        <td>合计：</td>
        <ng-container *ngFor="let tsRow of detailService.tableStyle">
          <td *ngIf="!tsRow.immutable" [nzAlign]="'right'">
            <ng-container *ngIf="tsRow.statistics;else elseBlock">
              {{ detailService.statistics[tsRow.field_name]?.label }}
            </ng-container>
            <ng-template #elseBlock></ng-template>
          </td>
        </ng-container>
        
      </tr>
    </tbody>

</nz-table>


<p-toast [style]="{marginTop: '80px'}" styleClass="primeng-custom-toast" key="custom" position="top-right"></p-toast>
    `
    //#endregion
    return ret217;
  }
  //#endregion

  //#region 生成Angular单表控制器代码
  createSingleController(tableName:string,modelName:string,detailService:string,packageName:string):string{
    // 下面的正则表达式写在字符串中
    // 由于\是转义字符，所以要结果字符串中出现该字符
    // 那么需要连续两个该符号
    let regSplit:string = '/\\+|\\-|\\*|\\//g';

    //#region 代码模板
    let ret1:string =`
import { Component, OnInit } from '@angular/core';
import { ${modelName}, ${detailService} } from 'src/app/service/life-schedule.service';
import { TableStyle, TableStyleService } from 'src/app/service/table-style.service';
import { NzNotificationService } from 'ng-zorro-antd';
import { BreadFieldService } from 'src/app/service/bread-field.service';
import { JsonResult } from 'src/app/model/JsonResult';
import { UUID } from 'angular2-uuid';
import { MessageService } from 'primeng/api';

@Component({
  selector: 'app-${packageName}',
  templateUrl: './${packageName}.component.html',
  styleUrls: ['./${packageName}.component.css'],
  providers: [ MessageService ]
})
export class ${modelName}Component implements OnInit {

  vari:{// 变量集合 - 属性集合
    'tableName':string,// 表名称
    'tableData':${modelName}[],// 表格组件的数据源
    'tableStyle':TableStyle[],// 列样式表
    'mouseOver_rowIndex':number,// 鼠标悬停的行号
    'emptyRowCount':number, // 空白行数
    'statistics':{ [key:string]:{ label:string,value:number } }, // 有统计的字段的集合
    'searchInputValue':string
  } = { 'tableName':'${tableName}','tableData':[],'tableStyle':[],
    mouseOver_rowIndex:-1,'emptyRowCount':1,
    'statistics':{},'searchInputValue':null};

  // 样式表 table_style 在字段 opts 上有值的字段的集合，构建为下拉数据源
  // 格式是：value1=label1,value2=label2...
  // 实例：  1=单程,2=往返
  // 同时实现在表格的对应列上出现下拉数据，将 value 保存到数据源，显示模式下显示 label
  opts_enum:{ [key:string]:{label:string,value:string}[] } = {};

  // 表格编辑用缓存数据
  cache:{
    detail:{ [key:string]:{ edit:boolean,data:${modelName} } }
  } = {
    'detail':{}
  }

  mouseOverUUID:string= '';

  constructor(
    public pMsgService:MessageService,
    public noti:NzNotificationService,
    public tableStyleService:TableStyleService,
    public detailService:${detailService},
    public breadField:BreadFieldService
  ) { 
    this.init();
  }

  ngOnInit() {
  }

  init(){
    this.initOpts();
    this.pullTableData();
  }

  // 使用表格数据更新编辑用缓存数据
  updateEditCache(){
    this.vari.tableData.forEach(
      (ele:${modelName}) => {
        this.cache.detail[ele.uuid] = {
          edit:false,data:{...ele}
        }
    });
    
  }

  // 初始化获取所有带选择数据和字段样式数据
  initOpts(){
    this.tableStyleService.selectByTableName(this.vari.tableName).subscribe(
      ( res:JsonResult ) =>{
        if( res.code !==1 ){
          this.msg(res.msg);
          return
        }

        this.vari.tableStyle = res.data;// 样式字段集合保存到属性中
        this.vari.tableStyle.forEach(ele => {
          // 在2020年2月8日21:10:38 从百分比宽度修改为 px 宽度
          // 记得表 table_style 中保留一个字段在 width_percent 是0
          ele.width_percent_str = ele.width_percent + 'px';
          this.resolveEnumOpts(ele);// 表 table_style 在字段 opts 上有值的话构建为下拉选择数据
        });
      },error => this.msg('获取表格样式数据时出现异常！')
    )
  }

  // 刷新表格数据 - 从远端获取数据更新本地视图
  pullTableData(){
    this.detailService.selectAll().subscribe(
      ( res:JsonResult ) =>{
        if( res.code !== 1 ){
          this.msg(res.msg);
          return
        }

        this.vari.tableData = res.data;
        // 遍历数组为 uuid 赋值
        this.vari.tableData.forEach(ele => {
          ele.uuid = UUID.UUID();
        });
        this.updateEditCache();// 更新编辑用缓存数据
        // this.buildEmptyRows();
        this.updateStatistics();// 更新统计数据
      },error => this.msg('获取表格数据时出现异常！')
    )
  }

  // 构建空白行
  buildEmptyRows(){
    if( this.vari.emptyRowCount === undefined ||this.vari.emptyRowCount === null || this.vari.emptyRowCount<=0 ) return

    let emptyRows:${modelName}[] = [];
    let lastUUID:string = '';
    for(let i=0;i<this.vari.emptyRowCount;i++){
      let newRecord = new ${modelName}();
      lastUUID = newRecord.uuid;
      emptyRows.push(newRecord); 
    }

    this.vari.tableData = this.vari.tableData.concat(emptyRows);
    this.updateEditCache();
    this.cache.detail[lastUUID].edit = true;
  }


  /**
   * 解析来自数据库端的枚举类型的选项，构建为本地数组
   * @param enums 格式为：value1=label1,value2=label2
   */
  resolveEnumOpts(record:TableStyle){
    let enums = record.opts;
    if( enums === undefined || enums === null || enums.length<=0 ) {
      return
    }
    if( record.immutable === true ) return // 是系统字段则不构建

    let arr = enums.split(',');
    let ret:{label:string,value:string}[] = [];
    arr.forEach(ele => {
      let val = ele.split('=');
      ret.push({ value:String(val[0]),label:val[1] });
    });

    this.opts_enum[record.field_name] = ret;
  }

  printOpts(){
    console.log(this.opts_enum['one_way']);
  }


  mouseOver(uuid:string){
    // this.mouseOverUUID = uuid;
  }

  mouseLeave(rowIndex:number){
    //this.msg('鼠标走开了 --- ' + rowIndex);
  }

  /**
   * 新增一个空行，并且处于编辑状态
   */
  addEmptyRow(){
    let record:${modelName} = new ${modelName}();
    let uuid:string = record.uuid;
    this.cache.detail[uuid] = {
      edit:true,data:record
    };

    //  let ret = this.vari.tableData.splice(this.vari.tableData.length,0,record);
    this.vari.tableData = this.vari.tableData.concat(record);
  }

  hasEditableRow():boolean{
    let data = this.cache.detail;
    if( data === undefined || data === null ) return false;

    for(let attr in this.cache.detail){
      if( this.cache.detail[attr].edit )
        return true;
    }

    return false;
  }

  startEdit(rowIndex:number,uuid:string,event){
    event.preventDefault();
    this.cache.detail[uuid].edit = true;
    // this.fillDefault(rowIndex,uuid);
    // this.msg('开启编辑状态的行是：' + rowIndex + ',' + uuid);
  }

  cancelEdit(rowIndex:number,uuid:string){
    this.cache.detail[uuid].edit = false;
  }

  saveEdit(rowIndex:number,uuid:string){
    let record:${modelName} = this.cache.detail[uuid].data;
    this.detailService.saveOne(record).subscribe(
      ( res:JsonResult ) => {
        if( res.code !=1 ){
          this.msg(res.msg);
          return
        }

        this.msg('保存完毕！');
        let newArray = this.vari.tableData.filter(
          item => item.uuid !== uuid
        )
        let newRecord:${modelName} = res.data;
        console.log('修改后返回的对象是：');
        console.log(newRecord);
        newRecord.uuid = UUID.UUID();
        newArray.splice(rowIndex,0,{...newRecord})
        this.vari.tableData = newArray;
        this.updateEditCache();// 更新编辑用缓存数据
        this.updateStatistics();// 更新统计数据
        this.cache.detail[newRecord.uuid].edit = false;
      },error => this.msg('保存时出现异常！')
    )

  }

  // 删除行 - 仅前端
  deleteRow_front(rowIndex:number,uuid:string){
    this.vari.tableData = this.vari.tableData.filter(
      item => item.uuid !== uuid
    )
  }
  
  /**
   * 删除一行数据：
   * 1. 如果没有iid ，那么只要删除前端 + 更新编辑用缓存数据
   * 2. 删除后端成功后，删除前端 + 更新编辑用缓存数据
   * @param rowIndex 行序号，从0开始
   * @param uuid 行属性uuid
   */
  deleteRow(rowIndex:number,uuid:string){
    let iid = this.vari.tableData[rowIndex].iid;
    if( iid === undefined || iid === null || iid<=0 ){// 仅仅删除前端
      this.deleteRow_front(rowIndex,uuid);
      this.updateEditCache();
      this.updateStatistics();
    }else{// 删除后端成功后：删除前端 + 更新编辑用缓存
      this.detailService.deleteByPrimaryKey(this.vari.tableData[rowIndex].iid).subscribe(
        ( res:JsonResult ) => {
          if( res.code != 1 ){
            this.msg(res.msg);
            return
          }
  
          this.deleteRow_front(rowIndex,uuid);
          this.updateEditCache();
          this.updateStatistics();
          this.msg('删除完毕！');
        },error => this.msg('删除时出现异常！')
      )
    }
  }

  cancelDeleteRow(){
    // console.log('取消了删除行');
  }

  copyRow(rowIndex:number,uuid:string){
    let iid = this.vari.tableData[rowIndex].iid;
    if( iid === undefined || iid === null || iid <=0 ){
      this.msg('该行还未保存，不可复制！');
      return
    }

    let newRecord:${modelName} = {...this.vari.tableData[rowIndex]};
    newRecord.iid = 0;
    this.detailService.saveOne(newRecord).subscribe(
      ( res:JsonResult ) =>{
        if( res.code !==1 ){
          this.msg(res.msg);
          return
        }

        newRecord.uuid = UUID.UUID();
        this.vari.tableData = this.vari.tableData.concat(newRecord);
        this.updateEditCache();
        this.msg('复制完毕！');
      },error => this.msg('复制时出现异常！')
    )
  }


/**
 * 将数据库 table_style 表中设置为枚举类型的下拉选项的字段
 * 显示为其值对应的文本
 * @param record 行数据，单个实体类对象
 * @param fieldName 字段名称
 */
  convertRaw2Show(record:${modelName},tsRow:TableStyle):string{
    return this.breadField.convertRaw2Show(record,tsRow);
  }


  // 从显示模式切换到编辑模式时为下拉框填充数据
  fillDefault(rowIndex:number,uuid:string){
    console.log('进入fillDefault');
    this.vari.tableStyle.forEach(ele => {
      switch(ele.ctl_type){
        case('optsEnum'):
          console.log('是 optsEnum 类型的控件，赋值前：' + this.cache.detail[uuid].data[ele.field_name]);
          this.cache.detail[uuid].data[ele.field_name] = this.vari.tableData[rowIndex][ele.field_name];
          console.log('赋值后：' + this.cache.detail[uuid].data[ele.field_name]);
          break;
        default:
          break;
      }
    });
    
  }

  setTest(){
    let aa:string = '1+2-3*4/2';
    const gg = ${regSplit};
    let bb = aa.split(gg);

    console.log(bb);
  }

  // 最初的设计是视图中存在数据表中没有的字段，完全从其他字段通过逻辑或者算数运算得来
  // 后来发现统计上会有问题，放弃该做法，采用较死板的方式：
  // 算数运算中在等于号左边的字段也要在数据表中有字段存储数据，在编辑模式中为不可编辑。
  generateCol(dataRow:${modelName},tsRow:TableStyle){
    return this.breadField.getGeneratedResult(dataRow,tsRow);
  }

  colHeaderAlign(fieldName:string):string{
    let tsRow:TableStyle = this.vari.tableStyle.filter( item => item.field_name === fieldName )[0];
    return tsRow.align_header;
  }

  colContentAlign(fieldName:string):string{
    let tsRow:TableStyle = this.vari.tableStyle.filter( item => item.field_name === fieldName )[0];
    return tsRow.align_cell;
  }

  // 更新统计数据
  updateStatistics(){
    if( this.vari.tableData.length<=0 ) return

    this.vari.statistics = null;
    this.vari.statistics = {};

    // 遍历字段构建汇总对象
    let arr = this.vari.tableStyle.filter(item=>item.statistics);
    if( arr.length<=0 ) return;
    arr.forEach(ele => {
      this.vari.statistics[ele.field_name] = { value:0,label:'' };
    });
    

    this.vari.tableData.forEach(ele => {
      this.vari.tableStyle.forEach(ele1 => {
        let field:string = ele1.field_name;
        if( ele1.statistics ){
          let curValue:number = ele[field] === undefined?0:ele[field];
          curValue = curValue === null?0:curValue;
          this.vari.statistics[field].value = this.vari.statistics[field].value + curValue;
          this.vari.statistics[field].label = Number(this.vari.statistics[field].value).toFixed(1);
        }
      });
    });
  }


  refreshTable(){
    this.pullTableData();
  }


  resolveFormula(dataRow:${modelName},tsRow:TableStyle):string{
    if( !tsRow.statistics ) return

    return this.breadField.useFormatter(dataRow,tsRow)
  }

  // 编辑状态下，编辑文本变动则对应修改其相关公式关联的字段的值
  inputChange(dataRow:${modelName},tsRow:TableStyle){
    let arr:{ fieldName:string,value:number }[] = this.breadField.triggerFormula(dataRow,tsRow,this.vari.tableStyle);
    if( arr === undefined || arr === null || arr.length<=0 ) return
    arr.forEach(ele => {
      this.cache.detail[dataRow.uuid].data[ele.fieldName] = ele.value;
    });
  }
  

  //#region 消息通知
  msg(content:string) {
    this.pMsgService.add({key: 'custom', severity:'info', summary: '通知', detail:content});
  }
  //#endregion

}
`
//#endregion

    //#region 代码模板 - 217版本
    let ret217:string = `
import { Component, OnInit } from '@angular/core';
import { ${modelName}, ${detailService} } from 'src/app/service/${packageName}.service';
import { TableStyle, TableStyleService } from 'src/app/service/table-style.service';
import { MessageService } from 'primeng/api';
import { NzNotificationService } from 'ng-zorro-antd';
import { BreadFieldService } from 'src/app/service/bread-field.service';
import { JsonResult } from 'src/app/model/JsonResult';
import { UUID } from 'angular2-uuid';
import { forkJoin, Subject } from 'rxjs';
import { ${detailService} } from 'src/app/service/${packageName}.service';
import { debounce, debounceTime } from 'rxjs/operators';

@Component({
  selector: 'app-${packageName}',
  templateUrl: './${packageName}.component.html',
  styleUrls: ['./${packageName}.component.css']
})
export class ${modelName}Component implements OnInit {
  ngOnInit(): void {}

  vari:{// 变量集合 - 属性集合
    'mouseOver_rowIndex':number,// 鼠标悬停的行号
    'emptyRowCount':number, // 空白行数
    'statistics':{ [key:string]:{ label:string,value:number } }, // 有统计的字段的集合
    'reqTip':string
  } = { mouseOver_rowIndex:-1,'emptyRowCount':1,'statistics':{},'reqTip':'请求失败，请检查网络连接！'};

  mouseOverUUID:string= '';

  constructor(
    private pMsgService:MessageService,
    private noti:NzNotificationService,
    private tableStyleService:TableStyleService,
    public detailService:${detailService},
    public breadField:BreadFieldService,
  ) { 
    this.init();
  }

  //#region 初始化表格数据、样式数据、表格编辑状态下的下拉数据
  init(){
    // 查询获取表格编辑状态下的下拉数据
    this.detailService.initOpts().subscribe(
      ( res:JsonResult ) =>{
        if( res.code !==1 ){
          this.msg('初始化下拉数据时出现异常！');
          return
        }
        
      },error => this.msg('初始化下拉数据时出现异常！')
    )

    this.refresh();// 查询获取表格数据、样式数据
  }
  //#endregion

  //#region 刷新表格数据
  refresh(){
    this.detailService.refresh().subscribe(
      ( res:JsonResult ) =>{
        if( res.code !==1 ){
          this.msg(res.msg);
          return
        }

        this.msg('刷新完毕！');
      },error => this.msg('刷新数据时出现异常！')
    )
  }
  //#endregion

  //#region 鼠标移动相关的事件
  mouseOver(uuid:string){
  }

  mouseLeave(rowIndex:number){
  }
  //#endregion

  //#region 新增一个空行
  addEmptyRow(){
    this.detailService.addEmptyRow();
  }
  //#endregion

  //#region 开启编辑状态
  startEdit(rowIndex:number,uuid:string,event){
    event.preventDefault();
    const res:string = this.detailService.startEdit(uuid);
    if( res.length>0 )
      this.msg(res);
  }
  //#endregion

  //#region 取消编辑
  cancelEdit(rowIndex:number,uuid:string){
    this.detailService.editCache[uuid].edit = false;
  }
  //#endregion

  //#region 删除行，兼容纯前端行、前后端行
  deleteRow(rowIndex:number,uuid:string){
    this.detailService.deleteRow(rowIndex,uuid).subscribe(
      ( res:JsonResult ) =>{
        if( res.code !==1 ){
          this.msg('删除数据时出错！');
          return
        }

        this.msg('删除成功！');
      },error => this.msg('删除数据时出现异常！')
    )
  }
  //#endregion

  cancelDeleteRow(){
    // console.log('取消了删除行');
  }

  //#region 指定字段 input 变动触发其他业务逻辑
  inputChange(dataRow:${modelName},tsRow:TableStyle){
    
    if( tsRow.field_name === 'amount' ){
      // 指定字段的 input 变动触发其他业务逻辑
    }

    if( tsRow.field_name === 'credit_amount' ){
      // 指定字段的 input 变动触发其他业务逻辑
    }

  }
  //#endregion

  //#region 变动触发
  onChange_category(e){
    // 变动触发
  }

  onChange_enum(e:string,fieldName:string){
    // 变动触发
  }
  //#endregion

  //#region 过滤
  filter(opts:string[],fieldName:string){
    this.detailService.filter(opts,fieldName);
  }
  //#endregion

  //#region 自定义彩色消息通知
  msg(content:string) {
    this.pMsgService.add({key: 'custom', severity:'info', summary: '通知', detail:content});
  }
  //#endregion

  //#region 保存编辑行
  saveEdit(rowIndex:number,uuid:string){
    this.detailService.saveEdit(rowIndex,uuid).subscribe(
      ( res:JsonResult ) =>{
        if( res.code !==1 ){
          this.msg('保存时出现异常！');
          return
        }

        this.msg('保存完毕！');
      },error => this.msg('保存时出现异常！')
    )
  }
  //#endregion

  printTD(){
    //console.log(this.detailService.filterOpts);
  }

  //#region 恢复删除
  restoreDeleted(rowIndex:number,uuid:string){
    // this.detailService.restoreDeleted(this.detailService.tableDataDisplay[rowIndex].iid,rowIndex).subscribe(
    //   ( res:JsonResult ) =>{
    //     if( res.code !==1 ){
    //       this.msg(res.msg);
    //       return
    //     }
    //
    //     this.msg('恢复成功！');
    //   },error => this.msg('恢复被删除数据时出现异常！')
    // )
  }
  //#endregion
  
  // 单表控制器过滤手动输入的模糊查询
  searchInput(){
    this.filter([this.vari.searchInputValue],'company_name');
  }

  resetSearchInput(){
    this.filter([],'company_name');
  }

}
    `
    //#endregion


    // 在2020年1月18日22:57:39第二次制作，所以返回 ret1
    // 在2020年2月17日10:04:31第三次制作，主要业务逻辑放到服务中
    return ret217;
  }
  //#endregion

  //#region 生成Angular模块导入代码
  createModuleImports():string{
    let ret:string = `
// 将下面的代码拷贝到模块的顶部
import { NgZorroAntdModule } from 'ng-zorro-antd';
import { FormsModule,ReactiveFormsModule } from '@angular/forms';
import { ToastModule } from 'primeng/toast';

// 将下面的代码拷贝到模块的 imports 中
FormsModule,ReactiveFormsModule,NgZorroAntdModule,ToastModule

// 下面的代码拷贝到CSS文件中
/* 彩色消息通知 */
:host ::ng-deep .primeng-custom-toast .ui-toast-message {
    color: #ffffff;
    background: #FC466B;
    background: -webkit-linear-gradient(to right, #3F5EFB, #FC466B);
    background: linear-gradient(to right, #3F5EFB, #FC466B);
}

:host ::ng-deep .primeng-custom-toast .ui-toast-close-icon {
    color: #ffffff;
}

// 下面的代码拷贝到HTML模板文件中的最后
<p-toast [style]="{marginTop: '80px'}" styleClass="primeng-custom-toast" key="custom" position="top-right"></p-toast>

// 下面的代码拷贝到 ts 文件中
import { MessageService } from 'primeng/api';
providers: [ MessageService ]
private pMsgService:MessageService,
//#region 消息通知
msg(content:string) {
  this.pMsgService.add({key: 'custom', severity:'info', summary: '通知', detail:content});
}
//#endregion
    `

    return ret;
  }
  //#endregion

  //#region SQL主键字段构成的字符串获取对应记录集
  createSQL_pk2Records(tableName:string,pkFieldName:string):string{
    let ret:string = `
    set @pks='1,2,3';
    SELECT b.*
    FROM
    (
        SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(@pks,',',help_topic_id+1),',',-1) AS pk
        FROM mysql.help_topic
        WHERE help_topic_id < LENGTH(@pks)-LENGTH(REPLACE(@pks,',',''))+1
    ) a
    LEFT JOIN ${tableName} b
    ON a.pk = b.${pkFieldName}
    `;
    return ret;
  }
  //#endregion

  // 创建Mybatis ResultMap
  createMybatisResultMap(dbConfig:DBConfig,nodeKey:number,prefix:string,suffix:string):Observable<string>{
    const dbName:string = this.getDBNameByKey(nodeKey);
    const tableName:string = this.getTableNameByKey(nodeKey);

    let ret:string = `<resultMap id="CustomResultMap" type="com.ccsoft.gsms.model.BillMain">\n`;
    prefix = this.stringService.toRealString(prefix);
    suffix = this.stringService.toRealString(suffix);
    let fieldName:string = '';
    let dataType:string = '';
    return this.getTableFields(dbConfig,dbName,tableName).pipe(map(
      ( res:MySQLColumnInfo[] ) =>{
        if( !res ) return '';

        res.forEach(ele => {
          fieldName = prefix + ele.COLUMN_NAME + suffix;
          dataType = this.dbDataType2MybatisDataType[ele.DATA_TYPE];
          console.log('原始字段：' + ele.COLUMN_NAME + '，添加前后缀：' + fieldName);
          console.log('原数据类型：' + ele.DATA_TYPE + '，映射后是：' + dataType);

          ret = ret + `<result column="${fieldName}" jdbcType="${dataType}" property="${fieldName}"/>\n`;
        });
        
        ret = ret + `</resultMap>`;
        return ret;
      }
    ))
  }

  // 带有间隔符号的字符串为每个首字母大写
  toUpperWords(val:string,delimiter?:string):string{
    // 默认为下划线
    if( delimiter === undefined || delimiter === null || delimiter.length <=0 )
      delimiter = '_';

    if( val === undefined || val === null || val.length <=0 )
      return '';

    let strArr:string[] = val.split(delimiter);
    if( strArr.length <=0 ) return '';

    let ret = '';
    strArr.forEach(ele => {
      ret = ret + ele.charAt(0).toUpperCase() + ele.slice(1);
    })

    return ret;
  }

  // 创建单据序时表服务类
  createChroRptService(jControllerName:string,mainTableName:string,detailTableName:string,
    billName:string,// 使用 cli 生成的服务类似：xx.service，这里填写 xx
    mainPrefix?:string,detailPrefix?:string){
    // 默认主表前缀是 m_，明细表前缀是 d_
    if( mainPrefix === undefined || mainPrefix === null ) mainPrefix = 'm_';
    if( detailPrefix === undefined || detailPrefix === null ) detailPrefix = 'd_';

    if( !jControllerName || !mainTableName || !detailTableName ) return
    if( jControllerName.length<=0 || mainTableName.length<=0 || detailTableName.length<=0 ) return

    // 单据主表：bill_main，明细表是：bill_detail，他们融合的单据模型类是：BillExplode
    // 但是其对应的服务是：bill.service
    // 所以本方法的第四个传入参数 billName 应该传入bill
    // 首字母大写并且添加后缀后是：BillExplode，对应的服务是 bill.service
    const explodeModel:string = this.toUpperWords(billName) + 'Explode';

    let ret:string = `
import { Injectable } from '@angular/core';
import * as gc from 'src/app/global/const';
import { ${explodeModel} } from './${billName}.service';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { JsonResult } from '../model/JsonResult';
import { map } from 'rxjs/operators';
import { DatePipe } from '@angular/common';
import { TableStyleService, TableStyle } from './table-style.service';
import { UUID } from 'angular2-uuid';
import { NzFilterOpts } from '../global/model';

@Injectable({
  providedIn: 'root'
})
export class RptChroBillService {

  private jController:string = '${jControllerName}';// 对应java后端控制器的 RequestMap 映射名称
  data:${explodeModel}[] = [];// 从后端获取到的原始数据
  displayData:${explodeModel}[] = [];// 显示到UI的数据
  oStyle:TableStyle[] = [];// 字段样式原始数据
  prefixStyle:TableStyle[] = [];// 将字段添加前缀为了匹配数据字段
  statistics:{ [key:string]:{ label:string,value:number } } = {};// 统计数据

  // 用于过滤的非重数据数组
  // 列字段为 Map 的 Key，非重数据为带选择数组
  filterOpts:{ [fieldName:string]:NzFilterOpts[] } = {};// Map 类型的过滤用数据

  // 执行过滤时的条件 - 按照这些条件进行过滤
  conditions:{ [fieldName:string]:string[] } = {};// 执行过滤时的条件

  // 排序用 map
  mapOfSort: { [key: string]: string | null } = {};// 用于排序的条件，实际没有使用到

  private url = {
    'refreshRpt' : gc.BACKEND_SERVER + '/' + this.jController + '/refreshRpt'
  }
  constructor(
    private http:HttpClient,
    private datePipe:DatePipe,
    private tableStyleService:TableStyleService
  ) { 
	/**
	 * 初始化样式数据：
	 * 1. 从样式数据表 table_style 中获取主表+明细表的样式数据
	 * 2. 遍历上一步获取到的数据构建其副本，同时构建带有前缀的样式数据，主表的样式在字段前面添加 m_，明细表添加前缀 d_
	 * 3. 构建过滤用 Map 类型数据的键
	 * 4. 构建排序用 Map 类型数据的键 - 实际后来没有用到
	 */
    this.initRpt();
  }

  // 初始化用于排序的 Map
  initMapOfSort(data:TableStyle[]){
    if( !this.isArrayValid(data) ){
      // console.log('样式数据为空，初始化排序map失败！');
      return
    }

    data.forEach(ele => {
      this.mapOfSort[ele.field_name] = null;
    });
  }

	// 向过滤用 Map 结构中填充非重数据
  initFilterOpts(){
    if( !this.data || this.data.length<=0 ) {
      // console.log('报表表格数据为空，无法初始化过滤数据！');
      return
    }

    this.filterOpts = {};
    
    const optStyles:TableStyle[] = this.prefixStyle.filter( item => item.filter === 'multiple' || item.filter === 'single' );
    if( optStyles.length <=0 ) {
      // console.log('样式数据中没有符合 multiple 、single的数据，完整数据是：');
      // console.log(this.prefixStyle);
      return
    }

    optStyles.forEach(ele => {
      this.filterOpts[ele.field_name] = [];
    });

    // console.log('开始遍历表格数据构建去重的过滤用数据数组');
    let tmp:NzFilterOpts = null;
    this.data.forEach(ele => {
      optStyles.forEach(ele1 => {
        tmp = new NzFilterOpts();
        tmp.text = ele[ele1.display_field];
        tmp.value = ele[ele1.field_name];
        
        if( this.isExist(this.filterOpts[ele1.field_name],tmp) ){
          // console.log('table_style.iid=' + ele1.iid + '，行的数据在数组中已经存在，不会再追加进去，当前该字段的非重数据是：');
          // console.log(this.filterOpts[ele1.field_name]);
        }else{
          // console.log('table_style.iid=' + ele1.iid + '，行的数据即将追加的对象是：');
          // console.log(tmp);
          this.filterOpts[ele1.field_name] = this.filterOpts[ele1.field_name].concat(tmp);
        }
        
      });
    });

    // console.log('最终构建出的用于过滤的数组是：');
    // console.log(this.filterOpts);
  }

  // 初始化过滤用 Map 结构的键
  initFilterOptsKeys(data:TableStyle[]){
    if( !this.isArrayValid(data) ) {
      // console.log('由于样式数据为空，无法初始化过滤用数据的键集！');
      return
    }

    data.forEach(ele => {
      this.filterOpts[ele.field_name] = [];
    });
  }

  // 初始化报表服务类
  // 1. 请求获取主表、明细表字段样式数据
  initRpt():Observable<JsonResult>{
    const tableNames:string = '${mainTableName},${detailTableName}';
    return this.tableStyleService.selectMultiTables(tableNames).pipe(map(
      ( jr:JsonResult ) =>{
        if( jr.code !==1 ){
          return jr;
        }
        
        this.oStyle = [];
        this.prefixStyle = jr.data;
        let oTmp:TableStyle = null;
        // 由于单据序时表将原始字段添加了前缀，那么从样式表 table_style 
        // 获取到数据后要按照同样的规则添加前缀，才能将样式和数据对应起来
        if( this.prefixStyle && this.prefixStyle.length>0 ){
          this.prefixStyle.forEach(ele => {

            // 保留原始数据，用于用户自定义
            // 行数据只有属性，不存在深拷贝问题
            oTmp = new TableStyle();
            oTmp = {...ele};
            oTmp.uuid = UUID.UUID();
            this.oStyle.push({...oTmp});

            // 为字段添加前缀
            if( ele.table_name === '${mainTableName}' ) {
              ele.field_name = 'm_' + ele.field_name;
              ele.display_field = 'm_' + ele.display_field;
            }
            if( ele.table_name === '${detailTableName}' ) {
              ele.field_name = 'd_' + ele.field_name;
              ele.display_field = 'd_' + ele.display_field;
            }
          });

          this.initFilterOptsKeys(this.prefixStyle);// 初始化过滤MAP的KEY
          this.initMapOfSort(this.prefixStyle);// 初始化排序MAP的KEY
        }

        
        return jr;
      }
    ))
  }

  // 根据时间段查询获取单据序时表数据
  refreshRpt(sdate:Date,edate:Date):Observable<JsonResult>{
    const dateFormat:string = 'yyyy-MM-dd HH:mm:ss';
    const sdateString = this.datePipe.transform(sdate,dateFormat);
    const edateString = this.datePipe.transform(edate,dateFormat);
    const url = this.url.refreshRpt + '/' + sdateString + '/' + edateString;
    
    return this.http.get<JsonResult>(url).pipe(map(
      ( jr:JsonResult ) =>{
        if( jr.code === 1 ) {
          this.data = this.deepCopy(jr.data);
          this.displayData = this.deepCopy(jr.data);
          this.initFilterOpts();// 初始化过滤用项目数组
          this.updateStatistics();// 更新统计数据
        }
        return jr;
      }
    ))
  }


  // 保存字段样式
  saveStyle(data:TableStyle[]):Observable<JsonResult>{
    // 重置序号字段 seq 之后再保存
    for(let i=0;i<data.length;i++){
      data[i].seq = i+1;
      // console.log('设置了' + data[i].iid + '-' + data[i].field_name + '-' + data[i].text + '的seq=' + data[i].seq);
    }

    return this.tableStyleService.saveBatch(data).pipe(map(
      ( jr:JsonResult ) =>{
        return jr;
      }
    ))
  }

  // 更新统计数据
  updateStatistics(){
    const tableData:${explodeModel}[] = this.displayData;
    if( !tableData ) return;

    let fieldName:string = '';
    let value:number = 0;
    let label:string = '';

    const statFields = this.prefixStyle.filter( item => item.statistics );
    if( !statFields ) return;

    // 初始化合计数据数组
    statFields.forEach(ele => {
      this.statistics[ele.field_name] = { value:0,label:'0' };
    });

    // 遍历数据数组为每个字段统计
    tableData.forEach(ele => {
      statFields.forEach(el => {
        this.statistics[el.field_name].value = this.statistics[el.field_name].value + ele[el.field_name];
      });
    });

    // 遍历统计数据数组设置属性 label
    statFields.forEach(ele => {
      this.statistics[ele.field_name].label = this.statistics[ele.field_name].value.toFixed(ele.digit);
    });
  }

  // 过滤单个条件，这里的 fieldName 是带有前缀的字段
  // 或者将一个类型为 NzFilterOpts 的对象的两个属性分别拆分为下面两个传入参数
  private filterOne(fieldName:string,conditions:string[]){
    if( !fieldName ){
      // console.log('由于字段为空，不会进行过滤操作！');
      return
    }


    const curStyle:TableStyle = this.prefixStyle.filter(item => item.field_name === fieldName)[0];
    if( !curStyle ){
      // console.log('字段' + fieldName + '，在带有前缀的样式表数据中未找到！！！');
      return
    }

    if( curStyle.filter === 'multiple' ){// 多选过滤 - 相似，模糊
      this.displayData = this.displayData.filter(
        ( item:${explodeModel} ) =>{

          let fieldData:String = new String(item[fieldName]);
          return conditions.length? (conditions.some(// 传入了空的过滤条件则认为该字段上不做过滤，无条件返回所有数据
            ( val:string ) => fieldData.indexOf(val) !==-1
          ) ?true:false):true;
        }
      )
    }

    if( curStyle.filter === 'single' ){// 单选过滤 - 相等
      this.displayData = this.displayData = this.displayData.filter(
        ( item:${explodeModel} ) =>{
          let fieldData:String = new String(item[fieldName]);
          let opt:string = <string>(conditions[0]);
          return fieldData == opt;
        }
      )
    }

    if( curStyle.filter === 'input' ){// 输入过滤 - 模糊查询
      this.displayData = this.displayData.filter(
        ( item:${explodeModel} ) =>{
          let fieldData:String = new String(item[fieldName]);
          let opt:string = <string>(conditions[0]);
          opt = this.toRealString(opt);
          return opt.length?fieldData.indexOf(opt) !== -1:true;
        }
      )
    }


  }


  // 排序
  sort(fieldName:string,sortValue:string){
    this.displayData = this.displayData.sort((a, b) =>
        sortValue === 'ascend'
          ? a[fieldName!] > b[fieldName!]
            ? 1
            : -1
          : b[fieldName!] > a[fieldName!]
          ? 1
          : -1
    ).filter( item => 1===1 );
  }

  // 将任意类型的传入变量转换为字符串返回
  // 如果传入了 undefined、null 则返回空字符串
  toRealString(data:any):string{
    if( data === undefined ) return '';
    if( data === null ) return '';
    return data.toString();
  }


  filter(fieldName:string,data:any[]){
    // 设置全局过滤条件
    this.conditions[fieldName] = data;
    // console.log('本次过滤设置字段' + fieldName + '的查询条件是：');
    // console.log(data);
    // console.log('当前完整的过滤条件是：');
    // console.log(this.conditions);

    this.displayData = this.deepCopy(this.data);// 批量过滤之前恢复显示数据为原始数据
    for(let attr in this.conditions){// 遍历条件逐个进行过滤
      this.filterOne(attr,this.conditions[attr]);
    }
  }


  // 检测对象数组中是否存在某对象
  // 检车值相等，不是内存地址
  isExist(arr:any[],ele:any):boolean{
    // console.log('进入isExist');
    if( !arr || arr.length<=0 ) {
      // console.log('数据数组为空，不检测是否存在直接退出！');
      return false;
    }
    if( !ele ) {
      // console.log('用于检测的对象是空，不检测是否存在直接退出！');
      return false;
    }
    // console.log(arr);
    // console.log(ele);

    let check:boolean = true;
    const ret = arr.filter( item => {
      // console.log('检测的数组对象是：');
      // console.log(item);
      // console.log(ele);

      check = true;
      // console.log('已初始化为相等状态，鉴定对象是否相等');
      // console.log(item);
      // console.log(ele);
      for(let attr in item){
        // console.log(attr + ',属性对比:' + item[attr] + ','+ele[attr]);
        if (item[attr] != ele[attr]) {
          // console.log('两个对象的属性' + attr + '值不等，立即返回给调用者');
          check = false;
          return false;
        }else{
          console.log('两个对象的属性' + attr + '值相等');
          check = true;
        }
      }
      return check;
    })

    const result:boolean = ret.length>0?true:false;
    // console.log('如果上面出现过不等，那么你不应该能看到我，即将返回给调用者的结果是：' + result);
    // console.log(ret);
    return result;
  }


  // 排除方法的深度拷贝
  deepCopy(data){
    return JSON.parse(JSON.stringify(data));
  }

  // 检测数组是否有效：数组长度大于0为有效，返回true
  isArrayValid(arr:any[]){
    if( arr === undefined || arr === null || arr.length<=0 ) return false;
    return true;
  }

}`

    return ret;
  }


  // 创建单据序时表模板代码
  createChroRptHtml(){
    const ret:string = `
<div style="margin-bottom: 20px;">
<span>起止日期：</span>
<nz-range-picker [(ngModel)]="dateRange" (ngModelChange)="onChange_dateRange()"></nz-range-picker>&nbsp;
<nz-button-group [nzSize]="'default'">
    <button nz-button nzType="primary" (click)="refreshRpt()"><i nz-icon nzType="sync" nzTheme="outline"></i>刷新</button>
    <button nz-button nzType="default" (click)="printRpt()"><i nz-icon nzType="printer" nzTheme="outline"></i>打印</button>
    <button nz-button nzType="default" (click)="drawerVisible = !drawerVisible"><i nz-icon nzType="setting" nzTheme="outline"></i>设置</button>
</nz-button-group>
</div>


<nz-table #zTable nzBordered [nzData]="rptService.displayData" nzSize="small" [nzPageSize]="'1000'" 
    [nzHideOnSinglePage]="true" [nzScroll]="{ x: '3000px' }">
<thead>
    <tr class="strip">
        <th nzWidth="35px" nzLeft="0px">#</th>
        <ng-container *ngFor="let tsRow of prefixStyle">
            <ng-container *ngIf="tsRow.visible">
                <ng-container [ngSwitch]="tsRow.filter"><!-- 多分支构建过滤功能 -->
                    <th *ngSwitchCase="'multiple'" nzShowFilter [nzFilters]="rptService.filterOpts[tsRow.field_name]"
                        [nzFilterMultiple]="true"
                        (nzFilterChange)="rptService.filter(tsRow.field_name,$event)"
                        [nzAlign]="tsRow.align_header" 
                        [nzWidth]="tsRow.width_percent_str"
                        nzShowSort [(nzSort)]="rptService.mapOfSort[tsRow.field_name]" (nzSortChange)="rptService.sort(tsRow.field_name, $event)"
                        >
                        {{ tsRow.text }}
                    </th>

                        <th *ngSwitchCase="'single'" nzShowFilter [nzFilters]="rptService.filterOpts[tsRow.field_name]" 
                            [nzFilterMultiple]="false"
                            (nzFilterChange)="rptService.filter(tsRow.field_name,$event)"
                            [nzAlign]="tsRow.align_header"
                            [nzWidth]="tsRow.width_percent_str"
                            nzShowSort [(nzSort)]="rptService.mapOfSort[tsRow.field_name]" (nzSortChange)="rptService.sort(tsRow.field_name, $event)"
                            >
                            {{ tsRow.text }}
                        </th>

                        <th *ngSwitchCase="'input'" nzCustomFilter
                            nzShowSort [(nzSort)]="rptService.mapOfSort[tsRow.field_name]" (nzSortChange)="rptService.sort(tsRow.field_name, $event)"
                            >
                            {{ tsRow.text }}
                            <i nz-th-extra class="ant-table-filter-icon" nz-icon nz-dropdown
                              #dropdown="nzDropdown"
                              nzType="search"
                              [nzDropdownMenu]="inputFilterDropdown"
                              [class.ant-table-filter-open]="dropdown.nzVisible"
                              nzTrigger="click"
                              nzPlacement="bottomRight"
                              [nzClickHide]="true"
                              nzTableFilter
                            ></i>
                          </th>

                    <th *ngSwitchDefault [nzAlign]="tsRow.align_header" [nzWidth]="tsRow.width_percent_str"
                        nzShowSort [(nzSort)]="rptService.mapOfSort[tsRow.field_name]" (nzSortChange)="rptService.sort(tsRow.field_name, $event)"
                        >
                        {{ tsRow.text }}
                    </th>

                    <!-- 手动输入模糊查询的下拉菜单模板 -->
                    <nz-dropdown-menu #inputFilterDropdown="nzDropdownMenu">
                        <div class="search-box">
                            <input type="text" nz-input placeholder="输入模糊查询" [(ngModel)]="searchInputValue" />
                            <button nz-button nzSize="small" nzType="primary" (click)="rptService.filter(tsRow.field_name,[searchInputValue])" class="search-button">
                            过滤
                            </button>
                            <button nz-button nzSize="small" (click)="rptService.filter(tsRow.field_name,[])">重置</button>
                        </div>
                    </nz-dropdown-menu>

                </ng-container>

            </ng-container>
        </ng-container>
    </tr>
</thead>
<tbody>
    <tr *ngFor="let rowData of zTable.data;let rowIndex = index">
        <td nzWidth="35px" nzLeft="0px">{{ rowIndex + 1 }}</td>
        <ng-container *ngFor="let tsRow of rptService.prefixStyle">
            <ng-container *ngIf="tsRow.visible">

                <td [nzAlign]="tsRow.align_cell">
                    <span [ngSwitch]="tsRow.use_formatter">
                        <div *ngSwitchCase="'date'">
                            {{ rowData[tsRow.display_field] | date:tsRow.date_formatter }}
                        </div>

                        <div *ngSwitchCase="'digit'">
                            {{ rowData[tsRow.display_field] | number:tsRow.digit_formatter }}
                        </div>

                        <div *ngSwitchCase="'field'">
                            {{ rowData[tsRow.display_field] }}
                        </div>

                        <div *ngSwitchDefault>
                            {{ rowData[tsRow.display_field] }}
                        </div>
                    </span>
                </td>

            </ng-container>
        </ng-container>
    </tr>

    <!-- 统计行 -->
    <tr class="statRow">
        <td>*</td>
        <ng-container *ngFor="let tsRow of rptService.prefixStyle">
            <td *ngIf="!tsRow.immutable" [nzAlign]="'right'">
            <ng-container *ngIf="tsRow.statistics;else elseBlock">
                {{ rptService.statistics[tsRow.field_name]?.label }}
            </ng-container>
            <ng-template #elseBlock></ng-template>
            </td>
        </ng-container>
    </tr>

</tbody>
</nz-table>



<!-- 设置表格样式 -->
<nz-drawer [nzWidth]="1000"
    [nzClosable]="true"
    [nzVisible]="drawerVisible"
    nzPlacement="right"
    nzTitle="自定义表格样式"
    (nzOnClose)="drawerVisible = false"
>
    <nz-table nzBordered [nzData]="prefixStyle" nzSize="small" [nzPageSize]="'1000'" 
        [nzHideOnSinglePage]="true" [nzFrontPagination]="false" [nzShowPagination]="false"
        [nzScroll]="{ y: '400px' }">
        <thead>
            <tr class="strip">
                <th nzWidth="35px" nzLeft="0px">#</th>
                <th nzWidth="120px">字段名</th>
                <th nzWidth="0px">标题</th>
                <th nzWidth="75px">列宽</th>
                <th nzWidth="120px">列头对齐</th>
                <th nzWidth="120px">内容对齐</th>
                <th nzWidth="60px">可见</th>
            </tr>
        </thead>
        <tbody cdkDropList (cdkDropListDropped)="drop($event)">
            <ng-container *ngFor="let rowData of prefixStyle;let rowIndex = index">
                <!-- 显示模式 (click)="selectedRowUUID_tableStyleDetail = rowData.uuid"-->
                <tr *ngIf=" rowData['uuid'] !== selectedRowUUID_tableStyleDetail"
                    (click)="selectedRowUUID_tableStyleDetail = rowData.uuid"
                    [class.selectedRow]=" rowData['uuid'] === selectedRowUUID_tableStyleDetail" 
                    [class.strip]="rowIndex%2 !== 0" cdkDrag>

                    <td nzWidth="35px" nzLeft="0px">{{ rowIndex + 1 }}</td>
                    <td nzWidth="120px">{{ rowData.field_name }}</td>    <!-- 字段名称 -->
                    <td nzWidth="0px">{{ rowData.text }}</td>          <!-- 标题 -->
                    <td nzWidth="75px">{{ rowData.width_percent }}</td> <!-- 列宽 -->
                    <td nzWidth="120px">{{ rowData.align_header }}</td>  <!-- 列头对齐 -->
                    <td nzWidth="120px">{{ rowData.align_cell }}</td>    <!-- 内容对齐 -->
                    <td nzWidth="60px">
                        <span *ngIf="rowData.visible ===true;else tmpTableStyleDetailVisible">√</span>
                        <ng-template #tmpTableStyleDetailVisible>
                            <span>×</span>
                        </ng-template>
                    </td>
                </tr>

                <tr *ngIf=" rowData['uuid'] === selectedRowUUID_tableStyleDetail">
                    <td nzLeft="0px">{{ rowIndex + 1 }}</td>
                    <td>{{ rowData.field_name }}</td>
                    <td><input nz-input [(ngModel)]="rowData.text"></td>
                    <td><input nz-input [(ngModel)]="rowData.width_percent"></td>
                    <td>
                        <nz-select [(ngModel)]="rowData.align_header" style="width: 100%;">
                            <nz-option *ngFor="let item of globalEnum.zorroAlign" [nzLabel]="item" [nzValue]="item"></nz-option>
                        </nz-select>
                    </td>
                    <td>
                        <nz-select [(ngModel)]="rowData.align_cell" style="width: 100%;">
                            <nz-option *ngFor="let item of globalEnum.zorroAlign" [nzLabel]="item" [nzValue]="item"></nz-option>
                        </nz-select>
                    </td>
                    <td>
                        <label nz-checkbox [(ngModel)]="rowData.visible"></label>
                    </td>
                </tr>
            </ng-container>



        </tbody>
    </nz-table>

    <div class="footer" nz-row nzType="flex" nzJustify="space-between" nzAlign="middle">
        <div nz-col nzSpan="20" nzOffset="4">
            <button type="button" (click)="closeDrawer()" class="ant-btn" style="margin-right: 8px;"><span>取消</span></button>
            <button type="button" (click)="save_tableStyleDetail()" class="ant-btn ant-btn-primary"><span>保存</span></button>
        </div>
    </div>
</nz-drawer>



<p-toast [style]="{marginTop: '80px'}" styleClass="primeng-custom-toast" key="custom" position="top-right"></p-toast>
`
    return ret;
  }

  // 生成单据序时表CSS代码
  createChroRptCSS(){
    const ret:string = `
    /* 彩色消息通知 */
:host ::ng-deep .primeng-custom-toast .ui-toast-message {
    color: #ffffff;
    background: #FC466B;
    background: -webkit-linear-gradient(to right, #3F5EFB, #FC466B);
    background: linear-gradient(to right, #3F5EFB, #FC466B);
}

:host ::ng-deep .primeng-custom-toast .ui-toast-close-icon {
    color: #ffffff;
}

.strip {
    background-color: #eff4f9;
}

.selectedRow {
    background-color: #ffcf31;
}

/* 抽屉组件中下方的脚，用于存放保存、退出操作的按钮 */
.footer {
    position: absolute;
    bottom: 0px;
    width: 100%;
    border-top: 1px solid rgb(232, 232, 232);
    padding: 10px 16px;
    text-align: right;
    left: 0px;
    background: #fff;
}


/* 下面两个样式拖拽行进行排序时用到的 */
::ng-deep .cdk-drag-preview {
    display: table;
  }

::ng-deep .cdk-drag-placeholder {
    opacity: 0;
}


.statRow{
    background-color: a8a8a8;
}


/* 下面4个search开头的样式是列头过滤的样式 */
.search-box {
    padding: 8px;
  }

.search-box input {
    width: 188px;
    margin-bottom: 8px;
    display: block;
}

.search-box button {
    width: 90px;
}

.search-button {
    margin-right: 8px;
}`

    return ret;
  }


  // 生成单据序时表控制器代码
  createChroRptController(serviceName:string,billModel:string){

    // 传入首字母小写的单据融合类名称，例如：bill -> BillExplode，billDye -> BillDyeExplode
    const billModelName:string = this.toUpperWords(billModel) + 'Explode';

    // 将 rpt-chro-bill 转换为 RptChroBillService
    const serviceUpperName:string = this.toUpperWords(serviceName) + 'Service';

    const ret:string = `
import { Component, OnInit } from '@angular/core';
import { subDays } from 'date-fns';
import { RptChroBillService } from 'src/app/service/rpt-chro-bill.service';
import { JsonResult } from 'src/app/model/JsonResult';
import { MessageService } from 'primeng/api';
import { BillExplode } from 'src/app/service/bill.service';
import { TableStyle } from 'src/app/service/table-style.service';
import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';

@Component({
  selector: 'app-rpt-chro-bill',
  templateUrl: './rpt-chro-bill.component.html',
  styleUrls: ['./rpt-chro-bill.component.css'],
  providers: [ MessageService ]
})
export class RptChroBillComponent implements OnInit {

  dateRange:Date[] = [ subDays(new Date(),30),new Date() ];
  drawerVisible:boolean = false;
  selectedRowUUID_tableStyleDetail:string | null = null;
  prefixStyle:TableStyle[] = [];

  searchInputValue:string = '';// 列头input类型的过滤

  constructor(
    private rptService:RptChroBillService,
    private pMsgService:MessageService
  ) { 
    this.initCmpt();
  }

  ngOnInit() {
  }

  // 初始化本业务组件
  initCmpt(){
    this.rptService.initRpt().subscribe(
      ( jr:JsonResult ) =>{
        this.prefixStyle = this.rptService.prefixStyle;
      },error => this.msg('请求获取样式数据时出现异常！')
    )
    this.refreshRpt();

    // 延迟初始化过滤用数据
    setTimeout(() => {
      this.rptService.initFilterOpts();
    }, 3000);
  }

  // 刷新报表数据
  refreshRpt(){
    const sdate:Date = this.dateRange[0];
    const edate:Date = this.dateRange[1];

    this.rptService.refreshRpt(sdate,edate).subscribe(
      ( jr:JsonResult ) =>{
        // console.log('单据序时表查询获取到的表格数据是：');
        // console.table(this.rptService.data);
      },error => this.msg('请求数据时出现异常！')
    )
  }

  // 打印报表
  printRpt(){

  }

  // 设置表格样式
  save_tableStyleDetail(){
    this.rptService.saveStyle(this.prefixStyle).subscribe(
      ( jr:JsonResult ) =>{
        if( jr.code !==1 ){
          this.msg(jr.msg);
          return
        }

        this.drawerVisible = false;
      },error => this.msg('保存样式数据时出现异常！')
    )
  }

  drop(event: CdkDragDrop<string[]>): void {
    this.selectedRowUUID_tableStyleDetail = null;
    moveItemInArray(this.prefixStyle, event.previousIndex, event.currentIndex);
  }

  // 关闭抽屉组件
  closeDrawer(){
    console.log(this.prefixStyle);
    // this.drawerVisible = false;
  }

  searchInput(fieldName:string){
    this.rptService.filter(fieldName,[this.searchInputValue]);
  }

  resetSearchInput(e){
    console.log(e);
  }

  //#region 消息通知
  msg(content:string) {
    this.pMsgService.add({key: 'custom', severity:'info', summary: '通知', detail:content});
  }
  //#endregion
}
`
    return ret;
  }


  // 生成SQL语句 - 向表 table_style 插入数据 - 指定表名的所有字段的样式数据
  createSQL_insertTableStyle(dbName:string,tableName:string){
    const ret:string = `
SET @dbName = '${dbName}';
SET @tableName = '${tableName}';
SET @is_column = 0;
DELETE FROM table_style WHERE table_name = @tableName;
INSERT INTO table_style
(table_name,field_name,\`text\`,bool_column)
SELECT TABLE_NAME AS table_name,COLUMN_NAME AS field_name,
SUBSTRING_INDEX(t1.COLUMN_COMMENT,',',1) AS TEXT,b'0'
FROM information_schema.columns t1
WHERE table_schema=@dbName AND table_name=@tableName;
    `;

    return ret;
  }


  // 生成批量SQL语句 - 修改数据库内所有表的所有 varchar 类型字段的字符集
  createSQL_characterSet(dbName:string){
    const ret:string = `
# 设置数据库下所有表所有类型为varchar 字段的字符集为 utf8_general_ci
# 仅需要调整 femis 为目标数据库名称即可
SELECT CONCAT('ALTER TABLE \`', table_name, '\` MODIFY \`', column_name, '\` ', DATA_TYPE, '(', CHARACTER_MAXIMUM_LENGTH, ') CHARACTER SET UTF8 COLLATE utf8_general_ci', (CASE WHEN IS_NULLABLE = 'NO' THEN ' NOT NULL' ELSE '' END), ';')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = '${dbName}'
AND DATA_TYPE = 'varchar'
AND
(
    CHARACTER_SET_NAME != 'utf8'
    OR
    COLLATION_NAME != 'utf8_general_ci'
);`;

    return ret;
  }

  createSQL_emptyString(dbName:string){
    const ret:string = `
SELECT CONCAT('ALTER TABLE \`', table_name, '\` ALTER COLUMN \`', column_name, '\` ',' set default \\'\\'',
    (CASE WHEN IS_NULLABLE = 'NO' THEN ' NOT NULL' ELSE '' END), ';')
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = '${dbName}'
AND DATA_TYPE = 'varchar';`;
    return ret;
  }

}

//#region 模型 - 数据库
export class DBConfig{
  server:string;
  port:number;
  dbName:string;
  driver:string;
  dbUserName:string;
  dbUserPwd:string;
}
//#endregion

//#region 模型 - 动态查询参数
export class DynamicQueryParams{
  dbConfig:DBConfig;
  sql:string;

  constructor(){
    this.dbConfig = new DBConfig();
  }
}
//#endregion
